lib_shopware6_api ================= Version v2.0.6 as of 2023-11-13 see `Changelog`_ |build_badge| |codeql| |license| |jupyter| |pypi| |pypi-downloads| |black| |codecov| |cc_maintain| |cc_issues| |cc_coverage| |snyk| .. |build_badge| image:: https://github.com/bitranox/lib_shopware6_api/actions/workflows/python-package.yml/badge.svg :target: https://github.com/bitranox/lib_shopware6_api/actions/workflows/python-package.yml .. |codeql| image:: https://github.com/bitranox/lib_shopware6_api/actions/workflows/codeql-analysis.yml/badge.svg?event=push :target: https://github.com//bitranox/lib_shopware6_api/actions/workflows/codeql-analysis.yml .. |license| image:: https://img.shields.io/github/license/webcomics/pywine.svg :target: http://en.wikipedia.org/wiki/MIT_License .. |jupyter| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/bitranox/lib_shopware6_api/master?filepath=lib_shopware6_api.ipynb .. for the pypi status link note the dashes, not the underscore ! .. |pypi| image:: https://img.shields.io/pypi/status/lib-shopware6-api?label=PyPI%20Package :target: https://badge.fury.io/py/lib_shopware6_api .. badge until 2023-10-08: .. https://img.shields.io/codecov/c/github/bitranox/lib_shopware6_api .. badge from 2023-10-08: .. |codecov| image:: https://codecov.io/gh/bitranox/lib_shopware6_api/graph/badge.svg :target: https://codecov.io/gh/bitranox/lib_shopware6_api .. |cc_maintain| image:: https://img.shields.io/codeclimate/maintainability-percentage/bitranox/lib_shopware6_api?label=CC%20maintainability :target: https://codeclimate.com/github/bitranox/lib_shopware6_api/maintainability :alt: Maintainability .. |cc_issues| image:: https://img.shields.io/codeclimate/issues/bitranox/lib_shopware6_api?label=CC%20issues :target: https://codeclimate.com/github/bitranox/lib_shopware6_api/maintainability :alt: Maintainability .. |cc_coverage| image:: https://img.shields.io/codeclimate/coverage/bitranox/lib_shopware6_api?label=CC%20coverage :target: https://codeclimate.com/github/bitranox/lib_shopware6_api/test_coverage :alt: Code Coverage .. |snyk| image:: https://snyk.io/test/github/bitranox/lib_shopware6_api/badge.svg :target: https://snyk.io/test/github/bitranox/lib_shopware6_api .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black .. |pypi-downloads| image:: https://img.shields.io/pypi/dm/lib-shopware6-api :target: https://pypi.org/project/lib-shopware6-api/ :alt: PyPI - Downloads shopware6 higher level API client, based on `lib_shopware_api_base <https://github.com/bitranox/lib_shopware6_api_base>`_ this might be a good example for Your own API Client Functions - to be further extended ---- automated tests, Github Actions, Documentation, Badges, etc. are managed with `PizzaCutter <https://github .com/bitranox/PizzaCutter>`_ (cookiecutter on steroids) Python version required: 3.8.0 or newer tested on recent linux with python 3.8, 3.9, 3.10, 3.11, 3.12-dev, pypy-3.9, pypy-3.10 - architectures: amd64 `100% code coverage <https://codeclimate.com/github/bitranox/lib_shopware6_api/test_coverage>`_, flake8 style checking ,mypy static type checking ,tested under `Linux <https://github.com/bitranox/lib_shopware6_api/actions/workflows/python-package.yml>`_, automatic daily builds and monitoring ---- - `Try it Online`_ - `Usage`_ - `Usage from Commandline`_ - `Installation and Upgrade`_ - `Requirements`_ - `Acknowledgements`_ - `Contribute`_ - `Report Issues <https://github.com/bitranox/lib_shopware6_api/blob/master/ISSUE_TEMPLATE.md>`_ - `Pull Request <https://github.com/bitranox/lib_shopware6_api/blob/master/PULL_REQUEST_TEMPLATE.md>`_ - `Code of Conduct <https://github.com/bitranox/lib_shopware6_api/blob/master/CODE_OF_CONDUCT.md>`_ - `License`_ - `Changelog`_ ---- Try it Online ------------- You might try it right away in Jupyter Notebook by using the "launch binder" badge, or click `here <https://mybinder.org/v2/gh/{{rst_include. repository_slug}}/master?filepath=lib_shopware6_api.ipynb>`_ Usage ----------- Overview ======== - `API`_ - `Currency`_ - `DeliveryTime`_ - `Media`_ - `Product`_ - `Tax`_ - `Unit`_ ------------------- API === back to `Overview`_ .. code-block:: python class Shopware6API(object): def __init__(self, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False) -> None: """ :param config, type ConfShopware6ApiBase :param use_docker_test_container: if to use the docker test container >>> my_api=Shopware6API() >>> my_api_currency=my_api.currency >>> my_api_delivery_time=my_api.delivery_time >>> my_api_media=my_api.media >>> my_api_product=my_api.product >>> my_api_tax=my_api.tax >>> my_api_unit=my_api.unit """ Currency ======== back to `Overview`_ .. code-block:: python class Currency(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ >>> # Setup >>> my_api = Currency() """ .. code-block:: python @lru_cache(maxsize=None) def get_currency_id_by_iso_code(self, currency_iso_code: str = "EUR") -> str: """ :param currency_iso_code: the currency iso code, like 'EUR', 'CHF', ... :returns: the id of the currency record >>> # Setup >>> my_api = Currency() >>> # test get currency id >>> my_currency_id = my_api.get_currency_id_by_iso_code('EUR') >>> assert 32 == len(my_currency_id) >>> # test not existing (int) >>> my_api.get_currency_id_by_iso_code(currency_iso_code='not_existing') Traceback (most recent call last): ... FileNotFoundError: currency record with isoCode "not_existing" not found >>> # Test clear Cache - the Cache has to be cleared if currencies are inserted or deleted >>> my_api.get_currency_id_by_iso_code.cache_clear() """ .. code-block:: python def get_currencies(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all currency records - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Currency() >>> my_l_dict_data = my_api.get_currencies() """ DeliveryTime ============ back to `Overview`_ .. code-block:: python class DeliveryTime(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ :param admin_client: :param config: :param use_docker_test_container: >>> # Setup >>> my_api = DeliveryTime() """ .. code-block:: python def cache_clear_delivery_time(self) -> None: """ Cache of some functions has to be cleared if delivery_time records are inserted or deleted >>> # Setup >>> my_api = DeliveryTime() >>> # Test >>> my_api.cache_clear_delivery_time() """ .. code-block:: python @lru_cache(maxsize=None) def get_delivery_times(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all delivery-time records - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = DeliveryTime() >>> my_l_dict_data = my_api.get_delivery_times() """ .. code-block:: python def search_delivery_times(self, payload: PayLoad = None) -> List[Dict[str, Any]]: """ search delivery-time records >>> # Setup >>> my_api = DeliveryTime() >>> # insert article >>> ignore = my_api.search_delivery_times() """ .. code-block:: python @lru_cache(maxsize=None) def get_delivery_times_sorted_by_min_days(self) -> List[Dict[str, Any]]: """ returns a list of 'id' and 'name' of delivery_times, sorted by minimal time the key 'position' starts with 10, 20 .... :returns : [{'name': '...', 'id': '...', 'position': 10}, ...] >>> # Setup >>> my_api = DeliveryTime() >>> # Test >>> my_api.get_delivery_times_sorted_by_min_days() [{'name': '...', 'id': '...', 'position': 10}, ...] """ Media ===== back to `Overview`_ .. code-block:: python class Media(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ >>> # Setup >>> my_api = Media() """ .. code-block:: python def cache_clear_media(self) -> None: """ Cache of some functions has to be cleared if media is inserted or deleted >>> # Setup >>> my_api = Media() >>> # test >>> my_api.cache_clear_media() """ .. code-block:: python def cache_clear_media_folder(self) -> None: """ Cache of some functions has to be cleared if media_folders are inserted or deleted >>> # Setup >>> my_api = Media() >>> # test >>> my_api.cache_clear_media_folder() """ .. code-block:: python @staticmethod def calc_media_filename_from_product_number( product_number: Union[int, str], position: int, url: str, ) -> str: """ media_filenamescan only exist once - so we build the filename from product_number, position, and extension of the url :param product_number: :param position: :param url: we take the extension from here :return: >>> # Setup >>> my_api = Media() >>> # Test >>> my_api.calc_media_filename_from_product_number(product_number=123456789, position=1, url='something.jpg') '123456789_1.jpg' >>> my_api.calc_media_filename_from_product_number(product_number='test_get_media_filename_from_product_number', position=1, url='something.jpg') 'test_get_media_filename_from_product_number_1.jpg' """ .. code-block:: python @staticmethod def calc_new_media_id(media_filename: PathMedia) -> str: """ calculates a new media_id (to insert) from media_filename. since a media_filename (with extension) must only exist once in shopware6, we can calculate the is from that name. :param media_filename: filename (or url) with extension :return: >>> # Setup >>> my_api = Media() >>> # Test >>> my_new_media_id = my_api.calc_new_media_id(media_filename='123.jpg') >>> assert 32 == len(my_new_media_id) >>> # Test no extension >>> my_new_media_id = my_api.calc_new_media_id(media_filename='123') Traceback (most recent call last): ... ValueError: media_filename "123" must have an extension """ .. code-block:: python def calc_path_media_folder_from_product_number(self, product_number: Union[int, str]) -> str: """ get the path of the complete media folder for a given product_number. the directory structure will be created as follows : 'xxxx...' the md5-hash buil out of the product number conf_path_media_folder_root/xx/xx/xx/xxxxxxxxxxxxxxxxxxxxxxxxxx that gives us 16.7 Million directories, in order to spread products evenly in folders (sharding). >>> # Setup >>> my_api = Media() >>> # test >>> my_api.calc_path_media_folder_from_product_number(product_number=456789) '/Product Media/api_imported/e3/5c/f7/b66449df565f93c607d5a81d09' >>> # test2 >>> my_api.calc_path_media_folder_from_product_number(product_number='123456789abcdefg') '/Product Media/api_imported/94/08/f8/da307c543595e92ded30cf4193' """ .. code-block:: python def delete_media_by_id(self, media_id: str) -> None: """ :param media_id: the media_id :return: >>> # Setup >>> import time >>> my_api = Media() >>> my_media_folder_id = my_api.upsert_media_folders_by_path('/Product Media/test_delete_media_by_id') >>> # insert two medias >>> ignore1 = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_01_1280.jpg') >>> ignore2 = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_02_1280.jpg') >>> # Test delete >>> my_api.delete_media_by_id(media_id=my_api.get_media_id_by_media_filename(media_filename='test001_01_1280.jpg')) # noqa >>> my_api.delete_media_by_id(media_id=my_api.get_media_id_by_media_filename(media_filename='test001_02_1280.jpg')) # noqa >>> # teardown >>> my_api.delete_media_folder_by_path('/Product Media/test_delete_media_by_id', force=True) """ .. code-block:: python def delete_media_folder(self, media_folder_id: Optional[str], force: bool = False) -> None: """ delete a media folder. on force, also containing media is deleted DANGER - API DELETES FOLDERS RUTHLESS - including Subfolders and pictures :param media_folder_id: the folder to delete :param force: if True, delete even if there are Subfolders or Media in that folder :return: None >>> # Setup >>> my_api = Media() >>> # insert Folder >>> my_media_folder_id = my_api.upsert_media_folders_by_path('/Product Media/test_delete_media_folder') >>> assert True == my_api.is_media_folder_existing_by_path('/Product Media/test_delete_media_folder') >>> # delete the inserted Folder >>> my_api.delete_media_folder(media_folder_id=my_media_folder_id) >>> assert False == my_api.is_media_folder_existing_by_path('/Product Media/test_delete_media_folder') >>> # insert Folder with subfolder >>> my_media_sub_folder_id = my_api.upsert_media_folders_by_path('/Product Media/test_delete_media_folder/subfolder') >>> assert True == my_api.is_media_folder_existing_by_path('/Product Media/test_delete_media_folder/subfolder') >>> # can not delete non-empty Folder >>> my_media_folder_id = my_api.get_media_folder_id_by_path('/Product Media/test_delete_media_folder') >>> my_api.delete_media_folder(media_folder_id=my_media_folder_id) Traceback (most recent call last): ... OSError: media_folder_id "..." is not empty >>> # force-delete non-empty Folder >>> my_api.delete_media_folder(media_folder_id=my_media_folder_id, force=True) >>> assert False == my_api.is_media_folder_existing_by_path('/Product Media/test_delete_media_folder') >>> # try to delete Root Folder >>> my_api.delete_media_folder(media_folder_id=None) Traceback (most recent call last): ... OSError: the root folder can not be deleted """ .. code-block:: python def delete_media_folder_by_path(self, path_media_folder: PathMediaFolder, force: bool = False) -> None: """ delete a media folder by path DANGER - API DELETES FOLDERS RUTHLESS - including Subfolders and pictures :param path_media_folder: like '/Product Media/a000/000/001 :param force: if True, delete even if there are Subfolders or Media in that folder :return: None >>> # Setup >>> my_api = Media() >>> ignore = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_delete_media_folder_by_path/subfolder1/subfolder2/subfolder3') >>> # Test delete Empty Folder >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_delete_media_folder_by_path/subfolder1/subfolder2/subfolder3') >>> # Test delete Empty Folder without force >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_delete_media_folder_by_path/subfolder1') Traceback (most recent call last): ... OSError: media_folder "/Product Media/test_delete_media_folder_by_path/subfolder1" is not empty >>> # Test delete Folder with force >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_delete_media_folder_by_path', force=True) >>> assert False == my_api.is_media_folder_existing_by_path(path_media_folder='/Product Media/test_delete_media_folder_by_path') """ .. code-block:: python @lru_cache(maxsize=None) def get_media_folder_configuration_id_from_media_folder_name(self, media_folder_name: str = "Product Media", parent_id: Optional[str] = None) -> str: """ get the configuration_id of a media folder. this configuration_id can be passed to child folders, in order to inherit the configuration from the parent folder Parameter : media_folder_name: the name of the parent folder, like 'Product Media' parent_id : the parent id of the Folder :returns: the configuration id >>> # Setup >>> my_api = Media() >>> # test get 'Product Media' id >>> my_folder_configuration_id = my_api.get_media_folder_configuration_id_from_media_folder_name() >>> assert 32 == len(my_folder_configuration_id) >>> # test not existing (int) >>> my_api.get_media_folder_configuration_id_from_media_folder_name(media_folder_name='not_existing') Traceback (most recent call last): ... FileNotFoundError: media folder with name "not_existing" not found >>> # Test clear Cache -the Cache has to be cleared if media_folders are inserted or deleted >>> my_api.get_media_folder_configuration_id_from_media_folder_name.cache_clear() """ .. code-block:: python def get_media_folder_configurations(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all media_folder_configurations - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Media() >>> my_l_dict_data = my_api.get_media_folder_configurations() """ .. code-block:: python @lru_cache(maxsize=None) def get_media_folder_id(self, name: str, parent_id: Optional[str]) -> str: """ get the id of a media folder >>> # Setup >>> my_api = Media() >>> # Test get existing Folder >>> assert my_api.get_media_folder_id(name='Product Media', parent_id=None) # noqa >>> # Test get non-existing Folder >>> my_api.get_media_folder_id(name='not-existing', parent_id=None) # noqa Traceback (most recent call last): ... FileNotFoundError: media_folder, name: "not-existing", parent_id: "None" not found >>> # Test clear Cache -the Cache has to be cleared if media_folders are inserted or deleted >>> my_api.get_media_folder_id.cache_clear() """ .. code-block:: python @lru_cache(maxsize=None) def get_media_folder_id_by_path(self, path_media_folder: PathMediaFolder) -> Optional[str]: """ get the id of a media folder :param path_media_folder: path - for instance /Product Media/a000/000/001 >>> # Setup >>> my_api = Media() >>> my_folder_id = my_api.upsert_media_folders_by_path('/Product Media/test_get_media_folder_id_by_path/999/999') >>> # Test Existing >>> assert my_folder_id == my_api.get_media_folder_id_by_path('/Product Media/test_get_media_folder_id_by_path/999/999') >>> # Test Invalid >>> my_api.get_media_folder_id_by_path('not-existing-folder') Traceback (most recent call last): ... OSError: media_folder path "not-existing-folder" is invalid, it must be absolute >>> # Test Not Existing >>> my_api.get_media_folder_id_by_path('/not-existing-folder') Traceback (most recent call last): ... FileNotFoundError: media_folder path "/not-existing-folder" not found >>> # Test clear Cache -the Cache has to be cleared if media_folders are inserted or deleted >>> my_api.get_media_folder_id_by_path.cache_clear() >>> # Teardown >>> my_api.delete_media_folder_by_path('/Product Media/test_get_media_folder_id_by_path', force=True) """ .. code-block:: python def get_media_folders(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all media_folder - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Media() >>> my_l_dict_data = my_api.get_media_folders() """ .. code-block:: python def get_media_id_by_media_filename(self, media_filename: PathMedia) -> str: """ gets the media_id from media_folder_id and media_filename this can only work if the picture is already uploaded ! :param media_filename: the filename (with extension) as string, like 'test001_01_1280.jpg', or the url link that ends with '.../test001_01_1280.jpg' :return: >>> # Setup >>> my_api = Media() >>> my_media_folder_id = my_api.upsert_media_folders_by_path('/Product Media/test_get_media_id/999/999') >>> my_media_id = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # test existing Folder, existing Media >>> my_media_filename = 'test001_07_1280.jpg' >>> assert my_media_id == my_api.get_media_id_by_media_filename(media_filename=my_media_filename) >>> # test non-existing Media >>> my_media_filename = 'bat013_77_7777.jpg' >>> my_api.get_media_id_by_media_filename(media_filename=my_media_filename) Traceback (most recent call last): ... FileNotFoundError: media_filename: "bat013_77_7777.jpg" not found >>> # Teardown >>> my_api.delete_media_folder_by_path(path_media_folder = '/Product Media/test_get_media_id', force=True) """ .. code-block:: python def get_medias(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all media records - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Media() >>> my_l_dict_data = my_api.get_medias() """ .. code-block:: python def insert_media( self, media_folder_id: Union[str, None], url: str, media_alt_txt: Union[str, None] = None, media_title: Union[str, None] = None, media_filename: Optional[PathMedia] = None, upload_media: bool = True, ) -> str: """ creates a single "media record" and uploads the media from the url - the media filename is taken from the url if not provided note that the same media_filename must not exist twice in the shop, even if on different media folders ! this should only be used if You upload the media indipendently from products - otherwise You should use associations to update the product with one request - see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyMzA4NTUw-associationsundefined if upload_media == False, You can only rely on the returned media_id to find the inserted record - all other fields are "None" so the api functions is_media_existing, etc. will not work ! You need to store the media_id and upload the media to complete the record. :param media_folder_id: id des folders :param url: url des files zum hochladen :param media_alt_txt: optional, 'alt' :param media_title: optional, 'title' :param media_filename: optional, the filename (with extension) as string, like 'test001_01_1280.jpg', otherwise taken from url :param upload_media if to upload the media :return: the new Media ID see : https://shopware.stoplight.io/docs/admin-api/c2NoOjE0MzUxMjU3-media see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling >>> # Setup >>> my_api = Media() >>> my_media_folder_id = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_insert_media') >>> # insert media >>> ignore = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg', ... media_filename = 'test001_07_1280.jpg') >>> # insert media, without stating filename >>> ignore = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_08_1280.jpg') >>> # cleanup >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_insert_media', force=True) """ .. code-block:: python def insert_media_by_path(self, path_media: PathMedia, url: str, media_alt_txt: Union[str, None] = None, media_title: Union[str, None] = None) -> str: """ Inserts a Media by Path, and upload the media from the url. note that the same media_filename must not exist twice in the shop, even if on different media folders ! this should only be used if You upload the media indipendently from products - otherwise You should use associations to update the product with one request - see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyMzA4NTUw-associationsundefined since associations will only be upserted but not deleted we make following approach : - delete the product_media relations for a product - :param path_media: '/Product Media/a000/123/456/000123456_01_1280.jpg' :param url: url='https://pics.rotek.at/test/test003/bilder/test003_01_1280.jpg' :param media_alt_txt: optional :param media_title: optional :return: the new media id >>> # Setup >>> my_api = Media() >>> # insert media >>> ignore = my_api.insert_media_by_path(path_media='/Product Media/insert_media_by_path/test001_07_1280.jpg', ... url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # insert media, without stating filename >>> ignore = my_api.insert_media_by_path(path_media='/Product Media/insert_media_by_path/test001_08_1280.jpg', ... url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # cleanup >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/insert_media_by_path', force=True) """ .. code-block:: python def insert_media_folder_by_name_and_parent_id(self, name: str, parent_id: Optional[str], configuration_id: Optional[str] = None) -> None: """ insert a media folder :param name: the name of the folder :param parent_id: the id of the parent folder :param configuration_id: the folder configuration id. taken from parent folder if none :return: None >>> # Setup >>> my_api = Media() >>> # insert Folder >>> id_root = my_api.get_media_folder_id(name='Product Media', parent_id=None) # noqa >>> my_api.insert_media_folder_by_name_and_parent_id(name='test_insert_media_folder_by_name_and_parent_id', parent_id=id_root) >>> assert True == my_api.is_media_folder_existing_by_path('/Product Media/test_insert_media_folder_by_name_and_parent_id') >>> # delete the inserted Folder >>> my_api.delete_media_folder_by_path('/Product Media/test_insert_media_folder_by_name_and_parent_id') """ .. code-block:: python def is_media_existing(self, media_filename: str) -> bool: """ True if the media ID exists - the media_id is read from the filename or the filename of the url. filename needs to have extension for the media mime type :param media_filename: filename or url of the media (if the filename is the same like the name in the url) :return: >>> # Setup >>> my_api = Media() >>> # insert media >>> ignore01 = my_api.insert_media_by_path(path_media='/Product Media/test_is_media_existing/is_media_existing_01.jpg', \ url='https://pics.rotek.at/test/test001/bilder/test001_05_1280.jpg') >>> # test check exist >>> assert True == my_api.is_media_existing(media_filename='https://pics.rotek.at/test/test001/bilder/is_media_existing_01.jpg') >>> assert True == my_api.is_media_existing(media_filename='is_media_existing_01.jpg') >>> # test check not exist >>> assert False == my_api.is_media_existing(media_filename='does_not_exist.jpg') >>> # test no extension >>> my_api.is_media_existing(media_filename='no_extension') Traceback (most recent call last): ... ValueError: media "no_extension" does not have an extension >>> # cleanup >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_is_media_existing', force=True) """ .. code-block:: python def is_media_existing_by_media_id(self, media_id: str) -> bool: """ :param media_id: :return: >>> # Setup >>> my_api = Media() >>> my_media_id = my_api.insert_media_by_path(path_media='/Product Media/test_is_media_existing_by_media_id/is_media_existing_by_media_id.jpg', \ url='https://pics.rotek.at/test/test001/bilder/test001_05_1280.jpg') >>> # Test Existing >>> assert True == my_api.is_media_existing_by_media_id(my_media_id) >>> # Test not Existing >>> assert False == my_api.is_media_existing_by_media_id('0123456789') >>> # TearDown >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_existing_by_media_id', force=True) """ .. code-block:: python def is_media_folder_containing_subfolders(self, media_folder_id: Optional[str]) -> bool: """ :returns True if there is a subfolder in the media folder :param media_folder_id: :return: >>> # Setup >>> my_api = Media() >>> ignore = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_folder_containing_subfolders') >>> # Test subfolder existing >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/') >>> assert True == my_api.is_media_folder_containing_subfolders(media_folder_id=my_media_folder_id) >>> # test no Subfolder >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/Product Media/test_is_media_folder_containing_subfolders') >>> assert False == my_api.is_media_folder_containing_subfolders(media_folder_id=my_media_folder_id) >>> # test Media Folder not existing >>> my_api.is_media_folder_containing_subfolders(media_folder_id='0123456789') Traceback (most recent call last): ... FileNotFoundError: media_folder id "0123456789" not found >>> # teardown >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_is_media_folder_containing_subfolders') """ .. code-block:: python def is_media_folder_empty(self, media_folder_id: Optional[str]) -> bool: """ true if the media_folder does not contain any media files or subfolders :param media_folder_id: :return: >>> # Setup >>> my_api = Media() >>> ignore1 = my_api.insert_media_by_path(path_media='/Product Media/test_is_media_folder_empty_with_media/test003_01_1280.jpg', ... url='https://pics.rotek.at/test/test003/bilder/test003_01_1280.jpg') >>> ignore2 = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_with_subfolder/subfolder') >>> ignore3 = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_empty') >>> # test no subfolder, media files existing >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_with_media') >>> assert False == my_api.is_media_folder_containing_subfolders(media_folder_id=my_media_folder_id) >>> # Test subfolder existing, no media files >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_with_subfolder') >>> assert False == my_api.is_media_folder_empty(media_folder_id=my_media_folder_id) >>> # Test no subfolder, no media files existing >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_empty') >>> assert True == my_api.is_media_folder_empty(media_folder_id=my_media_folder_id) >>> # Test Folder not existing >>> my_api.is_media_folder_containing_subfolders(media_folder_id='0123456789') Traceback (most recent call last): ... FileNotFoundError: media_folder id "0123456789" not found >>> # Teardown >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_with_media', force=True) >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_with_subfolder', force=True) >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_empty', force=True) """ .. code-block:: python def is_media_folder_empty_by_path(self, path_media_folder: PathMediaFolder) -> bool: """ true if the media_folder does not contain any media files or subfolders :param path_media_folder: like '/Product Media/a000/000/001 :return: >>> # Setup >>> my_api = Media() >>> ignore1 = my_api.insert_media_by_path(path_media='/Product Media/test_is_media_folder_empty_by_path_with_media/test003_01_1280.jpg', ... url='https://pics.rotek.at/test/test003/bilder/test003_01_1280.jpg') >>> ignore2 = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_by_path_with_subfolder/subfolder') >>> ignore3 = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_by_path_empty') >>> # Test no subfolder, media files existing >>> assert False == my_api.is_media_folder_empty_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_by_path_with_media') >>> # Test subfolder existing, no media files >>> assert False == my_api.is_media_folder_empty_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_by_path_with_subfolder') >>> # Test no subfolder, no media files existing >>> assert True == my_api.is_media_folder_empty_by_path(path_media_folder='/Product Media/test_is_media_folder_empty_by_path_empty') >>> # test Folder not existing >>> my_api.is_media_folder_containing_subfolders(media_folder_id='0123456789') Traceback (most recent call last): ... FileNotFoundError: media_folder id "0123456789" not found >>> # Teardown >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_by_path_with_media', force=True) >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_by_path_with_subfolder', force=True) >>> my_api.delete_media_folder_by_path('/Product Media/test_is_media_folder_empty_by_path_empty', force=True) """ .. code-block:: python def is_media_folder_existing(self, media_folder_id: Optional[str]) -> bool: """ True if the folder exists, False if it does not exist :param media_folder_id: :return: >>> # Setup >>> my_api = Media() >>> # Test media_folder existing >>> my_media_folder_id=my_api.get_media_folder_id_by_path(path_media_folder='/Product Media') >>> assert True == my_api.is_media_folder_existing(media_folder_id=my_media_folder_id) >>> # Test media_folder not existing >>> assert False == my_api.is_media_folder_existing(media_folder_id='0123456789') """ .. code-block:: python def is_media_folder_existing_by_path(self, path_media_folder: PathMediaFolder) -> bool: """ True if the folder exists, False if it does not exist :param path_media_folder: like '/Product Media/a000/000/001 :return: >>> # Setup >>> my_api = Media() >>> # Test media_folder existing >>> assert True == my_api.is_media_folder_existing_by_path(path_media_folder='/Product Media') >>> # Test media_folder not existing >>> assert False == my_api.is_media_folder_existing_by_path(path_media_folder='/test_is_media_folder_existing_by_path/sub1/sub2') """ .. code-block:: python def is_media_in_media_folder(self, media_folder_id: Optional[str]) -> bool: """ :returns True if there is some media files in the media folder :param media_folder_id: >>> # Setup >>> my_api = Media() >>> ignore01 = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_is_media_in_media_folder_no_media') >>> ignore02 = my_api.insert_media_by_path(path_media='/Product Media/test_is_media_in_media_folder_with_media/test001_07_1280.jpg', ... url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # Test no Media in Folder >>> my_media_folder_id = my_api.get_media_folder_id_by_path('/Product Media/test_is_media_in_media_folder_no_media') >>> assert False == my_api.is_media_in_media_folder(media_folder_id = my_media_folder_id) >>> # Test Media in Folder >>> my_media_folder_id = my_api.get_media_folder_id_by_path('/Product Media/test_is_media_in_media_folder_with_media') >>> assert True == my_api.is_media_in_media_folder(media_folder_id = my_media_folder_id) >>> # Test Folder not existing >>> my_api.is_media_in_media_folder(media_folder_id = '01234567890') Traceback (most recent call last): ... FileNotFoundError: media_folder id "01234567890" not found >>> # Teardown >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_is_media_in_media_folder_no_media', force=True) >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_is_media_in_media_folder_with_media', force=True) """ .. code-block:: python def search_media_folders(self, payload: PayLoad = None) -> List[Dict[str, Any]]: """ get all the media folders >>> # Setup >>> my_api = Media() >>> # test >>> my_l_data_dict = my_api.search_media_folders() """ .. code-block:: python def search_medias(self, payload: PayLoad = None) -> List[Dict[str, Any]]: """ get all the media >>> # Setup >>> my_api = Media() >>> # insert article >>> ignore = my_api.search_medias() """ .. code-block:: python def update_media( self, media_folder_id: Union[str, None], url: str, media_alt_txt: Union[str, None] = None, media_title: Union[str, None] = None, media_filename: Optional[PathMedia] = None, upload_media: bool = True, ) -> str: """ find the media record by media_filename and media_folder_id, update Media "mediaFolderId", "alt" and "title" upload the image from url. if no "media_filename" is provided, the media filename is taken from the url. :param media_folder_id: folder id :param url: url of the file to upload :param media_alt_txt: 'alt' :param media_title: 'title' :param media_filename: the filename (with extension) as string, like 'test001_01_1280.jpg' :param upload_media: if to upload the media :return: the media_id see : https://shopware.stoplight.io/docs/admin-api/c2NoOjE0MzUxMjU3-media see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling >>> # Setup >>> my_api = Media() >>> my_media_folder_id = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_update_media') >>> # insert media >>> ignore01 = my_api.insert_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test001/bilder/test001_09_1280.jpg', ... media_filename = 'test001_09_1280.jpg') >>> # update media, with url different from filename >>> ignore02 = my_api.update_media(media_folder_id=my_media_folder_id, url='https://pics.rotek.at/test/test003/bilder/test003_01_1280.jpg', ... media_filename = 'test001_09_1280.jpg') >>> # cleanup >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_update_media', force=True) """ .. code-block:: python def upload_media_from_url(self, media_id: str, url: str, filename_suffix: str, filename_stem: str) -> None: """ uploads the media to an existing media_id note that the same media_filename must not exist twice in the shop, even if on different media folders ! :param media_id: the media id :param url: the url to upload the media from :param filename_suffix: the extension, like "jpg" :param filename_stem: the filename (without extension) :return: """ .. code-block:: python def upsert_media( self, product_number: Union[int, str], position: int, url: str, media_alt: Union[str, None] = None, media_title: Union[str, None] = None, upload_media: bool = True, ) -> str: """ Insert or updates the Media and its folder. On insert, the media_id is calculated from product_number media folders are created as needed if upload_media == False, You can only rely on the returned media_id to find the inserted record - all other fields are "None" so the api functions is_media_existing, etc. will not work ! You need to store the media_id and upload the media to complete the record. :param product_number: 9 digit rotek artikelnummer :param position: the position when sorting pictures :param url: :param media_alt: :param media_title: :param upload_media: :return: the new, or updated media_id >>> # Setup >>> my_api = Media() >>> my_api.conf_path_media_folder_root = '/Product Media/api_test_upsert_product_media' >>> my_url='https://pics.rotek.at/test/test001/bilder/test001_03_1280.jpg' >>> my_product_number = '997997997' >>> my_media_filename = my_api.calc_media_filename_from_product_number( ... product_number=my_product_number, position=1, url=my_url) >>> # Test media is not existing now >>> assert False == my_api.is_media_existing(media_filename=my_media_filename) >>> # Test media upsert (insert) >>> ignore01 = my_api.upsert_media(product_number=my_product_number, position=1, url=my_url) >>> assert True == my_api.is_media_existing(media_filename=my_media_filename) >>> # Test media upsert (update) >>> ignore02 = my_api.upsert_media(product_number=my_product_number, position=1, url=my_url) >>> assert True == my_api.is_media_existing(media_filename=my_media_filename) >>> assert ignore01 == ignore02 >>> # cleanup >>> my_api.delete_media_folder_by_path(my_api.conf_path_media_folder_root, force=True) """ .. code-block:: python def upsert_media_folders_by_path(self, path_media_folder: PathMediaFolder, configuration_id: Optional[str] = None) -> Optional[str]: """ upsert media folders - including the parents, exist is ok :param path_media_folder: like '/Product Media/a000/000/001 :param configuration_id: the folder configuration id. taken from parent folder if none :return: the id of the last created folder >>> # Setup >>> my_api = Media() >>> # Test >>> discard = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_insert_media_folder_by_path/subfolder1/subfolder2') >>> assert True == my_api.is_media_folder_existing_by_path(path_media_folder='/Product Media/test_insert_media_folder_by_path/subfolder1/subfolder2') >>> # test Exist = Ok >>> discard = my_api.upsert_media_folders_by_path(path_media_folder='/Product Media/test_insert_media_folder_by_path/subfolder1/subfolder2') >>> assert True == my_api.is_media_folder_existing_by_path(path_media_folder='/Product Media/test_insert_media_folder_by_path/subfolder1/subfolder2') >>> # Teardown >>> my_api.delete_media_folder_by_path(path_media_folder='/Product Media/test_insert_media_folder_by_path', force=True) """ Product ======= back to `Overview`_ .. code-block:: python @attrs.define class ProductPicture: """ dataclass to upsert a picture """ .. code-block:: python class Product(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ :param admin_client: :param config: :param use_docker_test_container: >>> # Setup >>> my_api = Product() """ .. code-block:: python @staticmethod def calc_new_product_id(product_number: Union[int, str]) -> str: """ :param product_number: :return: the new id >>> # Setup >>> my_api = Product() >>> # Test >>> my_new_product_id = my_api.calc_new_product_id(product_number='123') >>> my_new_product_id2 = my_api.calc_new_product_id(product_number='1234') >>> assert 32 == len(my_new_product_id) >>> assert my_new_product_id != my_new_product_id2 """ .. code-block:: python @staticmethod def calc_new_product_media_id(product_id: str, position: int) -> str: """ the new product_media_id is calculated from product_id and position :param product_id: :param position: :return: >>> # Setup >>> my_api = Product() >>> # Test >>> my_new_product_media_id = my_api.calc_new_product_media_id(product_id='123', position=0) >>> my_new_product_media_id2 = my_api.calc_new_product_media_id(product_id='123', position=1) >>> assert 32 == len(my_new_product_media_id) >>> assert my_new_product_media_id != my_new_product_media_id2 """ .. code-block:: python def cache_clear_product(self) -> None: """ Cache of some functions has to be cleared if articles are inserted or deleted >>> # Setup >>> my_api = Product() >>> # Test >>> my_api.cache_clear_product() """ .. code-block:: python def delete_product_by_id(self, product_id: str) -> None: """ :param product_id: :return: >>> # Setup >>> my_api = Product() >>> my_article_id = my_api.insert_product(name='rn-doctest-article', product_number='test_delete_article_by_id_001', price_brutto=Decimal(0), stock=0) >>> # delete_article >>> my_api.delete_product_by_id(product_id=my_article_id) """ .. code-block:: python @lru_cache(maxsize=None) def get_product_id_by_product_number(self, product_number: Union[int, str]) -> str: """ :param product_number: :return: >>> # Setup >>> my_api = Product() >>> my_payload = dal.Criteria(limit=1, page=1) >>> first_article = my_api._admin_client.request_get(request_url="product", payload=my_payload)["data"][0] >>> my_article_id = first_article['id'] >>> my_article_product_number = first_article['productNumber'] >>> # Test get article_id >>> assert my_article_id == my_api.get_product_id_by_product_number(product_number=my_article_product_number) >>> # test not existing (int) >>> my_api.get_product_id_by_product_number(product_number='get_article_id_by_product_number9999_not_existing') Traceback (most recent call last): ... FileNotFoundError: article with productNumber(mysql_artikelnummer) "..." not found >>> # test not existing (str) >>> my_api.get_product_id_by_product_number(product_number='not_existing') Traceback (most recent call last): ... FileNotFoundError: article with productNumber(mysql_artikelnummer) "not_existing" not found >>> # Test clear Cache - the Cache has to be cleared if products are inserted or deleted >>> my_api.get_product_id_by_product_number.cache_clear() """ .. code-block:: python def delete_product_media_relation_by_id(self, product_media_id: str) -> None: """ delete product-media relation - but not the media itself. :param product_media_id: :return: >>> # Setup >>> my_api = Product() >>> my_api.media.conf_path_media_folder_root = '/Product Media/api_test_delete_product_media_by_id' >>> product_number = 'test_delete_product_media_by_id' >>> my_url='https://pics.rotek.at/test/test001/bilder/test001_01_1280.jpg' >>> my_position = 10 >>> my_product_id = my_api.insert_product(name='rn-doctest-article', product_number=product_number, price_brutto=Decimal(0), stock=0) >>> my_media_id = my_api.media.upsert_media(product_number=product_number, position=my_position, url=my_url) >>> my_product_media_id = my_api.insert_product_media_relation(product_id=my_product_id, media_id=my_media_id, position=my_position) >>> # Test >>> assert True == my_api.is_media_used_in_product_media(media_id=my_media_id) >>> my_api.delete_product_media_relation_by_id(product_media_id=my_product_media_id) >>> assert False == my_api.is_media_used_in_product_media(media_id=my_media_id) >>> # Teardown >>> my_api.delete_product_by_id(product_id=my_product_id) >>> my_api.media.delete_media_folder_by_path(my_api.media.conf_path_media_folder_root, force=True) """ .. code-block:: python def delete_product_media_relations_by_product_number(self, product_number: Union[int, str]) -> None: """ Delete all product_media relations of a product , but not the media itself, because there will be a reorg which deletes unused pictures. it does not change the cover picture It is neccessary to delete the product_media_relations before updating them, because otherwise deletion of pictures on the source database would not be propagated. If someone need to update the product pictures very frequently on a huge amount of products, there might be more efficient (but much more complicated) methods. >>> # Setup >>> my_api = Product() >>> my_api.media.conf_path_media_folder_root = '/Product Media/api_test_delete_product_picture_relations' >>> my_product_number = 'api_test_delete_product_picture_relations' >>> my_url='https://pics.rotek.at/test/test001/bilder/test001_01_1280.jpg' >>> my_product_id = my_api.insert_product(name='test_del_prod_media_rel_by_prod_number_001', product_number=my_product_number, \ price_brutto=Decimal(0), stock=0) >>> my_position = 10 >>> my_media_id_10 = my_api.media.upsert_media(product_number=my_product_number, position=my_position, url=my_url) >>> my_product_media_id_10 = my_api.insert_product_media_relation(product_id=my_product_id, media_id=my_media_id_10, position=my_position) >>> my_position = 20 >>> my_media_id_20 = my_api.media.upsert_media(product_number=my_product_number, position=my_position, url=my_url) >>> my_product_media_id_20 = my_api.insert_product_media_relation(product_id=my_product_id, media_id=my_media_id_20, position=my_position) >>> # Test delete product_media_relations >>> assert True == my_api.is_media_used_in_product_media(media_id=my_media_id_10) >>> assert True == my_api.is_media_used_in_product_media(media_id=my_media_id_20) >>> my_api.delete_product_media_relations_by_product_number(product_number=my_product_number) >>> assert False == my_api.is_media_used_in_product_media(media_id=my_media_id_10) >>> assert False == my_api.is_media_used_in_product_media(media_id=my_media_id_20) >>> # Test delete product_media_relations - product not existing is ok >>> my_api.delete_product_by_id(product_id=my_product_id) >>> my_api.delete_product_media_relations_by_product_number(product_number=my_product_number) >>> # Teardown >>> my_api.media.delete_media_folder_by_path(my_api.media.conf_path_media_folder_root, force=True) """ .. code-block:: python def get_product_medias(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all product_media - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Product() >>> my_l_dict_data = my_api.get_product_medias() """ .. code-block:: python def get_products(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all articles back - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Product() >>> dict_data = my_api.get_products() >>> assert len(dict_data) > 5 """ .. code-block:: python def insert_product( self, name: str, product_number: Union[int, str], stock: int = 0, price_brutto: Decimal = Decimal("0.00"), price_netto: Decimal = Decimal("0.00"), tax_name: str = "Standard rate", currency_iso_code: str = "EUR", linked: bool = True, ) -> str: """ see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyMzA4NTUy-product-data#simple-payload :param name: 'Stromerzeuger GD4-1A-6000-5EBZ' :param product_number: productNumber, mysql_artikelnummer :param stock: Anzahl auf Lager (?) :param tax_name: default tax record ('Standard rate') :param price_brutto: this price is displayed to customers who see gross prices in the shop :param price_netto: this price is shown to customers who see net prices in the shop if the price_netto is 0.00 it will be calculated from brutto price with the tax rate of the 'tax_name' stated :param currency_iso_code: the currency isoCode like 'EUR', 'CHF', ... :param linked: this is a flag for the administration. If it is set to true, the gross or net counterpart is calculated when a price is entered in the administration. :return: the new product id >>> # Setup >>> my_api = Product() >>> # insert article >>> my_new_product_id = my_api.insert_product(name='test_insert_product001', product_number='test_insert_article_by_product_number_999', ... price_brutto=Decimal(100), stock=0) >>> assert 32 == len(my_new_product_id) >>> # Teardown >>> my_api.delete_product_by_id(product_id=my_new_product_id) """ .. code-block:: python def upsert_product_payload(self, product_number: Union[int, str], payload: Dict[str, Any]) -> str: .. code-block:: python def insert_product_media_relation(self, product_id: str, media_id: str, position: int) -> str: """ inserts a single product_media Relation. the new product_media_relation_id is calculated from product_id and position this should only be used if You uploaded the media indipendently from products - otherwise You should use associations to update the product with one request - see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyMzA4NTUw-associationsundefined :param product_id: :param media_id: :param position: 0-based :return: the new product_media_relation_id >>> # Setup >>> my_api = Product() >>> my_new_product_id = my_api.insert_product(name='rn-doctest-article', product_number='test_insert_product_media_999') >>> my_new_media_id = my_api.media.insert_media_by_path( \ path_media='/Product Media/test_insert_product_media_999/test_insert_product_media_999_01_1280.jpg', \ url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # Test >>> my_new_product_media_id = my_api.insert_product_media_relation(product_id=my_new_product_id, media_id=my_new_media_id, position=0) >>> # Assert Media is used in product_media >>> assert True == my_api.is_media_used_in_product_media(media_id=my_new_media_id) >>> # Test delete Product, cascading delete to product_media >>> my_api.delete_product_by_id(product_id=my_new_product_id) >>> assert False == my_api.is_media_used_in_product_media(media_id=my_new_media_id) >>> # Teardown >>> my_api.media.delete_media_folder_by_path(path_media_folder = '/Product Media/test_insert_product_media_999/', force=True) """ .. code-block:: python def is_media_used_in_product_media(self, media_id: str) -> bool: """ :returns True if the media is used in a product :param media_id: >>> # Setup >>> my_api = Product() >>> my_new_product_id = my_api.insert_product(name='rn-doctest-article', product_number='test_is_media_used_in_product_media_999') >>> my_new_media_id = my_api.media.insert_media_by_path( ... path_media='/Product Media/test_is_media_used_in_product_media_999/test_is_media_used_in_product_media_999_01_1280.jpg', ... url='https://pics.rotek.at/test/test001/bilder/test001_07_1280.jpg') >>> # Test >>> my_new_product_media_id = my_api.insert_product_media_relation(product_id=my_new_product_id, media_id=my_new_media_id, position=0) >>> # Assert Media is used in product_media >>> assert True == my_api.is_media_used_in_product_media(media_id=my_new_media_id) >>> # Test delete Product, cascading delete to product_media >>> my_api.delete_product_by_id(product_id=my_new_product_id) >>> assert False == my_api.is_media_used_in_product_media(media_id=my_new_media_id) >>> # Teardown >>> my_api.media.delete_media_folder_by_path(path_media_folder = '/Product Media/test_is_media_used_in_product_media_999', force=True) """ .. code-block:: python def is_product_number_existing(self, product_number: Union[int, str]) -> bool: """ :param product_number: :return: >>> # Setup >>> my_api = Product() >>> my_new_product_id = my_api.insert_product(name='test_is_product_number_existing', product_number='is_product_number_existing_999') >>> # Test >>> assert True == my_api.is_product_number_existing(product_number = 'is_product_number_existing_999') >>> assert False == my_api.is_product_number_existing(product_number = 'product_number_does_not_exist') >>> # Teardown >>> my_api.delete_product_by_id(product_id=my_new_product_id) """ .. code-block:: python def search_product_medias(self, payload: PayLoad = None) -> List[Dict[str, Any]]: """ search product_media >>> # Setup >>> my_api = Product() >>> # insert article >>> ignore = my_api.search_product_medias() """ .. code-block:: python def upsert_product_pictures(self, product_number: Union[int, str], l_product_pictures: List[ProductPicture]) -> None: """ upsert product pictures and cover picture. The first picture (by Position Number) is automatically the cover picture :parameter product_number :parameter l_product_pictures list of Pictures >>> # Setup >>> my_api = Product() >>> my_api.media.conf_path_media_folder_root = '/Product Media/api_test_upsert_product_pictures' >>> my_product_number = 'test_upsert_product_pictures' >>> my_product_id = my_api.insert_product(name='test_upsert_product_pictures', product_number=my_product_number, price_brutto=Decimal(0), stock=0) >>> my_pictures=list() >>> my_pictures.append(ProductPicture(position=20, url='https://pics.rotek.at/test/test001/bilder/test001_02_1280.jpg', media_alt='', media_title='')) >>> my_pictures.append(ProductPicture(position=30, url='https://pics.rotek.at/test/test001/bilder/test001_03_1280.jpg', media_alt='', media_title='')) >>> my_pictures.append(ProductPicture(position=40, url='https://pics.rotek.at/test/test001/bilder/test001_04_1280.jpg', media_alt='', media_title='')) >>> my_pictures.append(ProductPicture(position=50, url='https://pics.rotek.at/test/test001/bilder/test001_05_1280.jpg', media_alt='', media_title='')) >>> my_pictures.append(ProductPicture(position=10, url='https://pics.rotek.at/test/test001/bilder/test001_01_1280.jpg', media_alt='', media_title='')) >>> # Test >>> my_api.upsert_product_pictures(product_number=my_product_number, l_product_pictures=my_pictures) >>> # Teardown >>> my_api.delete_product_media_relations_by_product_number(product_number=my_product_number) >>> my_api.delete_product_by_id(product_id=my_product_id) >>> my_api.media.delete_media_folder_by_path(my_api.media.conf_path_media_folder_root, force=True) """ Tax === back to `Overview`_ .. code-block:: python class Tax(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ :param admin_client: :param config: :param use_docker_test_container: >>> # Setup >>> my_api = Tax() """ .. code-block:: python def cache_clear_tax(self) -> None: """ Cache of some functions has to be cleared if tax is inserted or deleted >>> # Setup >>> my_api = Tax() >>> # test >>> my_api.cache_clear_tax() """ .. code-block:: python @lru_cache(maxsize=None) def get_tax_id_by_name(self, tax_name: str = "Standard rate") -> str: """ :param tax_name: the name of the tax record, like 'Standard rate', 'Reduced rate', 'Reduced Rate2' :returns: the id of the tax record >>> # Setup >>> my_api = Tax() >>> # test get 'Standard rate' id >>> my_tax_id = my_api.get_tax_id_by_name() >>> assert 32 == len(my_tax_id) >>> # test not existing (int) >>> my_api.get_tax_id_by_name(tax_name='not_existing') Traceback (most recent call last): ... FileNotFoundError: tax record with name "not_existing" not found >>> # Test clear Cache -the Cache has to be cleared if tax records are inserted or deleted >>> my_api.get_tax_id_by_name.cache_clear() """ .. code-block:: python def get_taxes(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all tax records - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Tax() >>> my_l_dict_data = my_api.get_taxes() """ .. code-block:: python @lru_cache(maxsize=None) def get_tax_rate_by_name(self, tax_name: str = "Standard rate") -> Decimal: """ :param tax_name: the name of the tax record, like 'Standard rate', 'Reduced rate', 'Reduced Rate2' :returns: the percent , like Decimal('19.00') >>> # Setup >>> my_api = Tax() >>> # test get 'Standard rate' percentage >>> my_tax_rate = my_api.get_tax_rate_by_name() >>> assert Decimal('19.00') == my_tax_rate >>> # test not existing (int) >>> my_api.get_tax_rate_by_name(tax_name='not_existing') Traceback (most recent call last): ... FileNotFoundError: tax record with name "not_existing" not found >>> # Test clear Cache -the Cache has to be cleared if tax records are inserted or deleted >>> my_api.get_tax_id_by_name.cache_clear() """ Unit ======== back to `Overview`_ .. code-block:: python class Unit(object): def __init__( self, admin_client: Optional[Shopware6AdminAPIClientBase] = None, config: Optional[ConfShopware6ApiBase] = None, use_docker_test_container: bool = False ) -> None: """ :param admin_client: :param config: :param use_docker_test_container: >>> # Setup >>> my_api = Unit() """ .. code-block:: python def cache_clear_unit(self) -> None: """ Cache of some functions has to be cleared if unit records are inserted or deleted >>> # Setup >>> my_api = Unit() >>> # Test >>> my_api.cache_clear_unit() """ .. code-block:: python @lru_cache(maxsize=None) def get_units(self, payload: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: """ get all delivery-time records - filters and so on can be set in the payload we read paginated (in junks of 100 items) - this is done automatically by function base_client.request_get_paginated() :parameters payload, to set filters etc. :returns l_dict_data, sample payload : page and limit will be overridden by function base_client.request_get_paginated() and will be ignored >>> # Setup >>> my_api = Unit() >>> # Test >>> my_l_dict_data = my_api.get_units() """ .. code-block:: python def search_units(self, payload: PayLoad = None) -> List[Dict[str, Any]]: """ search delivery-time records >>> # Setup >>> my_api = Unit() >>> # Test >>> ignore = my_api.search_units() """ Usage from Commandline ------------------------ .. code-block:: Usage: lib_shopware6_api [OPTIONS] COMMAND [ARGS]... use the shopware 6 api Options: --version Show the version and exit. --traceback / --no-traceback return traceback information on cli -h, --help Show this message and exit. Commands: info get program informations Installation and Upgrade ------------------------ - Before You start, its highly recommended to update pip and setup tools: .. code-block:: python -m pip --upgrade pip python -m pip --upgrade setuptools - to install the latest release from PyPi via pip (recommended): .. code-block:: python -m pip install --upgrade lib_shopware6_api - to install the latest release from PyPi via pip, including test dependencies: .. code-block:: python -m pip install --upgrade lib_shopware6_api[test] - to install the latest version from github via pip: .. code-block:: python -m pip install --upgrade git+https://github.com/bitranox/lib_shopware6_api.git - include it into Your requirements.txt: .. code-block:: # Insert following line in Your requirements.txt: # for the latest Release on pypi: lib_shopware6_api # for the latest development version : lib_shopware6_api @ git+https://github.com/bitranox/lib_shopware6_api.git # to install and upgrade all modules mentioned in requirements.txt: python -m pip install --upgrade -r /<path>/requirements.txt - to install the latest development version, including test dependencies from source code: .. code-block:: # cd ~ $ git clone https://github.com/bitranox/lib_shopware6_api.git $ cd lib_shopware6_api python -m pip install -e .[test] - via makefile: makefiles are a very convenient way to install. Here we can do much more, like installing virtual environments, clean caches and so on. .. code-block:: shell # from Your shell's homedirectory: $ git clone https://github.com/bitranox/lib_shopware6_api.git $ cd lib_shopware6_api # to run the tests: $ make test # to install the package $ make install # to clean the package $ make clean # uninstall the package $ make uninstall Requirements ------------ following modules will be automatically installed : .. code-block:: bash ## Project Requirements attrs>=21.3.0 click cli_exit_tools lib_detect_testenv lib_shopware6_api_base Acknowledgements ---------------- - special thanks to "uncle bob" Robert C. Martin, especially for his books on "clean code" and "clean architecture" Contribute ---------- I would love for you to fork and send me pull request for this project. - `please Contribute <https://github.com/bitranox/lib_shopware6_api/blob/master/CONTRIBUTING.md>`_ License ------- This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_ --- Changelog ========= - new MAJOR version for incompatible API changes, - new MINOR version for added functionality in a backwards compatible manner - new PATCH version for backwards compatible bug fixes v2.0.6 --------- 2023-11-13: - fix mypy error for PathLike v2.0.5 --------- 2023-07-14: - add codeql badge - move 3rd_party_stubs outside the src directory to ``./.3rd_party_stubs`` - add pypy 3.10 tests - add python 3.12-dev tests v2.0.4 --------- 2023-07-13: - require minimum python 3.8 - remove python 3.7 tests v2.0.3 --------- 2023-07-13: - introduce PEP517 packaging standard - introduce pyproject.toml build-system - remove setup.cfg - remove setup.py - update black config - clean ./tests/test_cli.py v2.0.2.4 --------- 2023-06-30: - update black config - remove travis config - remove bettercodehub config - do not upload .egg files to pypi.org - update github actions : checkout@v3 and setup-python@v4 - remove "better code" badges - remove python 3.6 tests - adding python 3.11 tests - update pypy tests to 3.9 v2.0.2.3 --------- 2022-06-30: specify correct "attr" version in requirements v2.0.2.2 --------- 2022-06-02: update to github actions checkout@v3 and setup-python@v3 v2.0.2.1 -------- 2022-06-01: update github actions test matrix v2.0.2 -------- 2022-03-29: remedy mypy Untyped decorator makes function "cli_info" untyped v2.0.1 -------- 2022-01-19: update documentation, enhance coverage v2.0.0 -------- 2022-01-19: add function is_product_number_existing, add Unit functions, changed some method names v1.0.2 -------- 2022-01-18: clean requirements.txt v1.0.1 -------- 2022-01-18: Documentation update, make PyPi package v1.0.0 -------- 2022-01-17: Initial Release
bitranox/lib_shopware6_api
higher level functions for shopware6 based on lib_shopware6_api_base
PythonMIT