From d341fc1da879d9d9233dd014321f9b99514873fd Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 22 Nov 2023 17:44:15 +0100 Subject: [PATCH 1/2] Add a pre-commit file based on InvenTree Removed those hooks which had to with non-python scripts --- .pre-commit-config.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9b9c4833 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: mixed-line-ending +- repo: https://github.com/pycqa/flake8 + rev: '6.1.0' + hooks: + - id: flake8 + additional_dependencies: [ + 'flake8-bugbear', + 'flake8-comprehensions', + 'flake8-docstrings', + 'flake8-string-format', + 'flake8-tidy-imports', + 'pep8-naming', + 'flake8-logging' + ] +- repo: https://github.com/pycqa/isort + rev: '5.12.0' + hooks: + - id: isort +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell From 7ea5ce690a1ff18b3c74b129b7bfb106a8a2ee3d Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 22 Nov 2023 18:20:05 +0100 Subject: [PATCH 2/2] Isort all existing files, remove lines with white-space (according to flake8). Spellcheck words Disable flake8 extras for now, as they are excessive --- .pre-commit-config.yaml | 10 +---- ci/check_version_number.py | 6 +-- inventree/api.py | 23 ++++++------ inventree/base.py | 53 +++++++++++++-------------- inventree/build.py | 2 +- inventree/company.py | 1 - inventree/currency.py | 27 +++++++------- inventree/label.py | 14 +++---- inventree/order.py | 20 +++++++--- inventree/part.py | 15 ++++---- inventree/project_code.py | 1 - inventree/purchase_order.py | 4 +- inventree/return_order.py | 4 +- inventree/sales_order.py | 20 +++++----- inventree/stock.py | 26 ++++++------- inventree/user.py | 1 - tasks.py | 14 +++---- test/test_api.py | 14 +++---- test/test_base.py | 9 +++-- test/test_build.py | 12 +++--- test/test_company.py | 7 ++-- test/test_currency.py | 4 +- test/test_internal_price.py | 5 ++- test/test_label.py | 3 +- test/test_order.py | 52 +++++++++++++------------- test/test_part.py | 73 +++++++++++++++++++------------------ test/test_project_codes.py | 8 ++-- test/test_stock.py | 28 +++++++------- 28 files changed, 229 insertions(+), 227 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b9c4833..6df36970 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,15 +13,6 @@ repos: rev: '6.1.0' hooks: - id: flake8 - additional_dependencies: [ - 'flake8-bugbear', - 'flake8-comprehensions', - 'flake8-docstrings', - 'flake8-string-format', - 'flake8-tidy-imports', - 'pep8-naming', - 'flake8-logging' - ] - repo: https://github.com/pycqa/isort rev: '5.12.0' hooks: @@ -30,3 +21,4 @@ repos: rev: v2.2.6 hooks: - id: codespell + args: ['-L fo'] diff --git a/ci/check_version_number.py b/ci/check_version_number.py index 883ba6cf..558e2c00 100644 --- a/ci/check_version_number.py +++ b/ci/check_version_number.py @@ -5,10 +5,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import sys -import re -import os import argparse +import os +import re +import sys if __name__ == '__main__': diff --git a/inventree/api.py b/inventree/api.py index fb6794f2..a6902a66 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -5,15 +5,14 @@ with the InvenTree database server. """ -import requests -import os import json import logging +import os +from urllib.parse import urljoin, urlparse +import requests from requests.auth import HTTPBasicAuth from requests.exceptions import Timeout -from urllib.parse import urljoin, urlparse - logger = logging.getLogger('inventree') @@ -39,7 +38,7 @@ def __init__(self, host=None, **kwargs): Args: base_url - Base URL for the InvenTree server, including port (if required) e.g. "http://inventree.server.com:8000" - + kwargs: username - Login username password - Login password @@ -77,7 +76,7 @@ def __init__(self, host=None, **kwargs): def setHostName(self, host): """Validate that the provided base URL is valid""" - + if host is None: raise AttributeError("InvenTreeAPI initialized without providing host address") @@ -86,7 +85,7 @@ def setHostName(self, host): if not url.scheme: raise Exception(f"Host '{host}' supplied without valid scheme") - + if not url.netloc or not url.hostname: raise Exception(f"Host '{host}' supplied without valid hostname") @@ -308,7 +307,7 @@ def request(self, api_url, **kwargs): auth = None else: auth = self.auth - + payload['headers'] = headers payload['auth'] = auth payload['proxies'] = proxies @@ -359,13 +358,13 @@ def request(self, api_url, **kwargs): if headers: detail['headers'] = headers - + if params: detail['params'] = params - + if files: detail['files'] = files - + if data: detail['data'] = data @@ -591,7 +590,7 @@ def downloadFile(self, url, destination, overwrite=False, params=None, proxies=d if headers: detail['headers'] = headers - + raise requests.exceptions.HTTPError(detail) headers = response.headers diff --git a/inventree/base.py b/inventree/base.py index 60ce2a99..fda28a49 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -import os -import logging import json +import logging +import os from . import api as inventree_api - INVENTREE_PYTHON_VERSION = "0.13.0" @@ -18,7 +17,7 @@ class InventreeObject(object): # API URL (required) for the particular model type URL = "" - + # Minimum server version for the particular model type REQUIRED_API_VERSION = None @@ -45,13 +44,13 @@ def __init__(self, api, pk=None, data=None): # extract it from the provided dataset if pk is None and data: pk = data.get('pk', None) - + # Convert to integer try: pk = int(pk) except Exception: raise TypeError(f"Supplied value ({pk}) for {self.__class__} is invalid.") - + if pk <= 0: raise ValueError(f"Supplier value ({pk}) for {self.__class__} must be positive.") @@ -70,7 +69,7 @@ def __init__(self, api, pk=None, data=None): @classmethod def checkApiVersion(cls, api): """Check if the API version supports this particular model. - + Raises: NotSupportedError if the server API version is too 'old' """ @@ -150,7 +149,7 @@ def pk(self): val = int(val) except ValueError: pass - + return val @classmethod @@ -164,7 +163,7 @@ def create(cls, api, data, **kwargs): data.pop('pk') response = api.post(cls.URL, data, **kwargs) - + if response is None: logger.error("Error creating new object") return None @@ -230,13 +229,13 @@ def save(self, data=None, files=None, method='PATCH'): """ self.checkApiVersion(self._api) - + # If 'data' is not specified, then use *all* the data if data is None: data = self._data - + if self._api: - + # Default method used is PATCH (partial update) if method.lower() == 'patch': response = self._api.patch(self._url, data, files=files) @@ -253,7 +252,7 @@ def save(self, data=None, files=None, method='PATCH'): self.reload() return response - + def is_valid(self): """ Test if this object is 'valid' - it has received data from the server. @@ -271,10 +270,10 @@ def is_valid(self): if data is None: return False - + if len(data) == 0: return False - + return True def reload(self): @@ -342,7 +341,7 @@ def bulkDelete(cls, api: inventree_api.InvenTreeAPI, items=None, filters=None): Returns: API response object - + Throws: NotImplementError: The server API version is too old (requires v58) ValueError: Neither items or filters are supplied @@ -354,12 +353,12 @@ def bulkDelete(cls, api: inventree_api.InvenTreeAPI, items=None, filters=None): if not items and not filters: raise ValueError("Must supply either 'items' or 'filters' argument") - + data = {} if items: data['items'] = items - + if filters: data['filters'] = filters @@ -372,7 +371,7 @@ def bulkDelete(cls, api: inventree_api.InvenTreeAPI, items=None, filters=None): class Attachment(BulkDeleteMixin, InventreeObject): """ Class representing a file attachment object - + Multiple sub-classes exist, representing various types of attachment models in the database. """ @@ -414,7 +413,7 @@ def upload(cls, api, attachment, comment='', **kwargs): 'attachment': (os.path.basename(attachment), fo), } ) - + else: # Assumes a StringIO or BytesIO like object name = getattr(attachment, 'name', 'filename') @@ -430,7 +429,7 @@ def upload(cls, api, attachment, comment='', **kwargs): logger.info(f"File uploaded to {cls.URL}") else: logger.error(f"File upload failed at {cls.URL}") - + return response def download(self, destination, **kwargs): @@ -473,7 +472,7 @@ def getMetadata(self): def setMetadata(self, data, overwrite=False): """Write metadata to this particular model. - + Arguments: data: The data to be written. Must be a dict object overwrite: If true, provided data replaces existing data. If false (default) data is merged with any existing data. @@ -558,7 +557,7 @@ class StatusMixin: - complete - cancel on supported items. - + Other functions, such as - ship - finish @@ -597,11 +596,11 @@ def _statusupdate(self, status: str, reload=True, data=None, **kwargs): return response def complete(self, **kwargs): - + return self._statusupdate(status='complete', **kwargs) def cancel(self, **kwargs): - + return self._statusupdate(status='cancel', **kwargs) @@ -614,14 +613,14 @@ class BarcodeMixin: @classmethod def barcodeModelType(cls): """Return the model type name required for barcode assignment. - + Default value is the lower-case class name () """ return cls.__name__.lower() def assignBarcode(self, barcode_data: str, reload=True): """Assign an arbitrary barcode to this object (in the database). - + Arguments: barcode_data: A string containing arbitrary barcode data """ diff --git a/inventree/build.py b/inventree/build.py index 440aea86..c4fbd4f5 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -14,7 +14,7 @@ class Build( def getAttachments(self): return BuildAttachment.list(self._api, build=self.pk) - + def uploadAttachment(self, attachment, comment=''): return BuildAttachment.upload( self._api, diff --git a/inventree/company.py b/inventree/company.py index 71dbd8b3..22ecd8e7 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -5,7 +5,6 @@ import inventree.base import inventree.order - logger = logging.getLogger('inventree') diff --git a/inventree/currency.py b/inventree/currency.py index a47f2895..4d39c4fc 100644 --- a/inventree/currency.py +++ b/inventree/currency.py @@ -2,7 +2,6 @@ import logging - logger = logging.getLogger('inventree') @@ -20,12 +19,12 @@ def __init__(self, api): self.base_currency = None self.exchange_rates = None - + def refreshExchangeRates(self): """Request the server update exchange rates from external service""" if self.api.api_version < 93: - raise ValueError(f"Server API version ({self.api.api_version}) is older than v93, which is required for manual exchange rate upates") + raise ValueError(f"Server API version ({self.api.api_version}) is older than v93, which is required for manual exchange rate updates") return self.api.post('currency/refresh/', {}) @@ -40,13 +39,13 @@ def updateFromServer(self): if response is None: logger.error("Could not retrieve currency data from InvenTree server") return - + self.base_currency = response.get('base_currency', None) self.exchange_rates = response.get('exchange_rates', None) if self.base_currency is None: logger.warning("'base_currency' missing from server response") - + if self.exchange_rates is None: logger.warning("'exchange_rates' missing from server response") @@ -55,20 +54,20 @@ def getBaseCurrency(self, cache=True): if not cache or not self.base_currency: self.updateFromServer() - + return self.base_currency - + def getExchangeRates(self, cache=True): """Return the exchange rate information from the server""" if not cache or not self.exchange_rates: self.updateFromServer() - + return self.exchange_rates def convertCurrency(self, value, source_currency, target_currency, cache=True): """Convert between currencies - + Arguments: value: The numerical currency value to be converted source_currency: The source currency code (e.g. 'USD') @@ -78,20 +77,20 @@ def convertCurrency(self, value, source_currency, target_currency, cache=True): # Shortcut if the currencies are the same if source_currency == target_currency: return value - + base = self.getBaseCurrency(cache=cache) rates = self.getExchangeRates(cache=cache) if base is None: raise AttributeError("Base currency information is not available") - + if rates is None: raise AttributeError("Exchange rate information is not available") - + if source_currency not in rates: raise NameError(f"Source currency code '{source_currency}' not found in exchange rate data") - + if target_currency not in rates: raise NameError(f"Target currency code '{target_currency}' not found in exchange rate data") - + return value / rates[source_currency] * rates[target_currency] diff --git a/inventree/label.py b/inventree/label.py index d42236f2..271a5fcf 100644 --- a/inventree/label.py +++ b/inventree/label.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- import logging +import os import inventree.base -import os logger = logging.getLogger('inventree') class LabelPrintingMixin: """Mixin class for label printing. - + Classes which implement this mixin should define the following attributes: LABELNAME: The name of the label type (e.g. 'part', 'stock', 'location') @@ -19,19 +19,19 @@ class LabelPrintingMixin: LABELNAME = '' LABELITEM = '' - + def printlabel(self, label, plugin=None, destination=None, *args, **kwargs): """Print the label belonging to the given item. - + Set the label with 'label' argument, as the ID of the corresponding label. A corresponding label object can also be given. - + If a plugin is given, the plugin will determine how the label is printed, and a message is returned. - + Otherwise, if a destination is given, the file will be downloaded to 'destination'. Use overwrite=True to overwrite an existing file. - + If neither plugin nor destination is given, nothing will be done """ diff --git a/inventree/order.py b/inventree/order.py index 37404d9a..31b01182 100644 --- a/inventree/order.py +++ b/inventree/order.py @@ -4,10 +4,18 @@ """ # Pass PurchaseOrder models through -from inventree.purchase_order import PurchaseOrder, PurchaseOrderAttachment, PurchaseOrderExtraLineItem, PurchaseOrderLineItem # noqa:F401 - -# Pass SalesOrder models through -from inventree.sales_order import SalesOrder, SalesOrderAttachment, SalesOrderExtraLineItem, SalesOrderLineItem, SalesOrderShipment # noqa:F401 - +from inventree.purchase_order import PurchaseOrder # noqa:F401 +from inventree.purchase_order import PurchaseOrderAttachment # noqa:F401 +from inventree.purchase_order import PurchaseOrderExtraLineItem # noqa:F401 +from inventree.purchase_order import PurchaseOrderLineItem # noqa:F401 # Pass ReturnOrder models through -from inventree.return_order import ReturnOrder, ReturnOrderAttachment, ReturnOrderExtraLineItem, ReturnOrderLineItem # noqa:F401 +from inventree.return_order import ReturnOrder # noqa:F401 +from inventree.return_order import ReturnOrderAttachment # noqa:F401 +from inventree.return_order import ReturnOrderExtraLineItem # noqa:F401 +from inventree.return_order import ReturnOrderLineItem # noqa:F401 +# Pass SalesOrder models through +from inventree.sales_order import SalesOrder # noqa:F401 +from inventree.sales_order import SalesOrderAttachment # noqa:F401 +from inventree.sales_order import SalesOrderExtraLineItem # noqa:F401 +from inventree.sales_order import SalesOrderLineItem # noqa:F401 +from inventree.sales_order import SalesOrderShipment # noqa:F401 diff --git a/inventree/part.py b/inventree/part.py index eb6b61c7..6fa66bee 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -4,11 +4,10 @@ import re import inventree.base -import inventree.stock -import inventree.company import inventree.build +import inventree.company import inventree.label - +import inventree.stock logger = logging.getLogger('inventree') @@ -21,7 +20,7 @@ class PartCategoryParameterTemplate(inventree.base.InventreeObject): def getCategory(self): """Return the referenced PartCategory instance""" return PartCategory(self._api, self.category) - + def getTemplate(self): """Return the referenced ParameterTemplate instance""" return ParameterTemplate(self._api, self.parameter_template) @@ -116,7 +115,7 @@ def getInternalPriceList(self): """ return InternalPrice.list(self._api, part=self.pk) - + def setInternalPrice(self, quantity: int, price: float): """ Set the internal price for this part @@ -182,7 +181,7 @@ def generateTestKey(cls, test_name): def getTestKey(self): return PartTestTemplate.generateTestKey(self.test_name) - + class BomItem(inventree.base.InventreeObject, inventree.base.MetadataMixin): """ Class representing the BomItem database model """ @@ -209,8 +208,8 @@ def setInternalPrice(cls, api, part, quantity: int, price: float): # Send the data to the server return api.post(cls.URL, data) - - + + class PartRelated(inventree.base.InventreeObject): """ Class representing a relationship between parts""" diff --git a/inventree/project_code.py b/inventree/project_code.py index 0fa759fb..bd9b7e90 100644 --- a/inventree/project_code.py +++ b/inventree/project_code.py @@ -3,7 +3,6 @@ import inventree.base - logger = logging.getLogger('inventree') diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index ca2bd620..89a6de36 100644 --- a/inventree/purchase_order.py +++ b/inventree/purchase_order.py @@ -3,8 +3,8 @@ """ import inventree.base -import inventree.part import inventree.company +import inventree.part class PurchaseOrder( @@ -55,7 +55,7 @@ def addExtraLineItem(self, **kwargs): def getAttachments(self): return PurchaseOrderAttachment.list(self._api, order=self.pk) - + def uploadAttachment(self, attachment, comment=''): return PurchaseOrderAttachment.upload( self._api, diff --git a/inventree/return_order.py b/inventree/return_order.py index 5d0f0349..6e7dee2b 100644 --- a/inventree/return_order.py +++ b/inventree/return_order.py @@ -3,8 +3,8 @@ """ import inventree.base -import inventree.part import inventree.company +import inventree.part import inventree.stock @@ -32,7 +32,7 @@ def getContact(self): def getLineItems(self, **kwargs): """Return line items associated with this order""" return ReturnOrderLineItem.list(self._api, order=self.pk, **kwargs) - + def addLineItem(self, **kwargs): """Create (and return) a new ReturnOrderLineItem against this order""" kwargs['order'] = self.pk diff --git a/inventree/sales_order.py b/inventree/sales_order.py index 3686b197..7c57c3cd 100644 --- a/inventree/sales_order.py +++ b/inventree/sales_order.py @@ -3,8 +3,8 @@ """ import inventree.base -import inventree.part import inventree.company +import inventree.part class SalesOrder( @@ -55,7 +55,7 @@ def addExtraLineItem(self, **kwargs): def getAttachments(self): return SalesOrderAttachment.list(self._api, order=self.pk) - + def uploadAttachment(self, attachment, comment=''): return SalesOrderAttachment.upload( self._api, @@ -63,10 +63,10 @@ def uploadAttachment(self, attachment, comment=''): comment=comment, order=self.pk, ) - + def getShipments(self, **kwargs): """ Return the shipments associated with this order """ - + return SalesOrderShipment.list(self._api, order=self.pk, **kwargs) def addShipment(self, reference, **kwargs): @@ -102,14 +102,14 @@ def getOrder(self): def allocateToShipment(self, shipment, stockitems=None, quantity=None): """ Assign the items of this line to the given shipment. - + By default, assign the total quantity using the first stock item(s) found. As many items as possible, up to the quantity in sales order, are assigned. - + To limit which stock items can be used, supply a list of stockitems to use in the argument stockitems. - + To limit how many items are assigned, supply a quantity to the argument quantity. This can also be used to over-assign the items, as no check for the amounts in the sales order is performed. @@ -144,11 +144,11 @@ def allocateToShipment(self, shipment, stockitems=None, quantity=None): items = list() for SI in stockitems: - + # Check if we are done if required_amount <= 0: continue - + # Check that this item has available stock if SI.quantity - SI.allocated > 0: thisitem = { @@ -211,7 +211,7 @@ def getOrder(self): def allocateItems(self, items=[]): """ Function to allocate items to the current shipment - + items is expected to be a list containing dicts, one for each item to be assigned. Each dict should contain three parameters, as follows: diff --git a/inventree/stock.py b/inventree/stock.py index 166fbfc3..c82b54b7 100644 --- a/inventree/stock.py +++ b/inventree/stock.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -import os import logging +import os import inventree.api import inventree.base -import inventree.part -import inventree.label import inventree.company +import inventree.label +import inventree.part class StockLocation(inventree.base.BarcodeMixin, inventree.base.MetadataMixin, inventree.label.LabelPrintingMixin, inventree.base.InventreeObject): @@ -30,7 +30,7 @@ def getParentLocation(self): if self.parent is None: return None - + return StockLocation(self._api, pk=self.parent) def getChildLocations(self, **kwargs): @@ -52,13 +52,13 @@ class StockItem(inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, inv @classmethod def adjustStockItems(cls, api: inventree.api.InvenTreeAPI, method: str, items: list, **kwargs): """Perform a generic stock 'adjustment' action. - + Arguments: api: InvenTreeAPI instance method: Adjument method, e.g. 'count' / 'add' items: List of items to include in the adjustment (see below) kwargs: Additional kwargs to send with the adjustment - + Items: Each 'item' in the 'items' list must be a dict object, containing the following fields: @@ -68,14 +68,14 @@ def adjustStockItems(cls, api: inventree.api.InvenTreeAPI, method: str, items: l if method not in ['count', 'add', 'remove', 'transfer', 'assign']: raise ValueError(f"Stock adjustment method '{method}' not supported") - + url = f"stock/{method}/" data = kwargs data['items'] = items return api.post(url, data=data) - + @classmethod def countStockItems(cls, api: inventree.api.InvenTreeAPI, items: list, **kwargs): """Perform 'count' adjustment for multiple stock items""" @@ -179,7 +179,7 @@ def removeStock(self, quantity, **kwargs): def transferStock(self, location, quantity=None, **kwargs): """Transfer this StockItem into the specified location. - + Arguments: location: A StockLocation instance or integer ID value quantity: Optionally specify quantity to transfer. If None, entire quantity is transferred @@ -188,7 +188,7 @@ def transferStock(self, location, quantity=None, **kwargs): if isinstance(location, StockLocation): location = location.pk - + if quantity is None: quantity = self.quantity @@ -230,7 +230,7 @@ def installStock(self, item, **kwargs): Arguments: stockItem: A stockItem instance or integer ID value - + kwargs: quantity: quantity of installed items notes: Optional transaction notes""" @@ -240,7 +240,7 @@ def installStock(self, item, **kwargs): item = item.pk else: quantity = kwargs.get('quantity', 1) - + if self._api.api_version >= 148: kwargs['quantity'] = kwargs.get('quantity', quantity) else: @@ -278,7 +278,7 @@ def getPart(self): def getLocation(self): """ Return the StockLocation associated with this StockItem - + Returns None if there is no linked StockItem """ diff --git a/inventree/user.py b/inventree/user.py index deca6470..dc255240 100644 --- a/inventree/user.py +++ b/inventree/user.py @@ -4,7 +4,6 @@ import inventree.base - logger = logging.getLogger('inventree') diff --git a/tasks.py b/tasks.py index 6fbeb6df..3b201248 100644 --- a/tasks.py +++ b/tasks.py @@ -6,11 +6,11 @@ from invoke import task import os +import sys import time import requests from requests.auth import HTTPBasicAuth -import sys @task @@ -46,7 +46,7 @@ def update_image(c, debug=True, reset=True): """ print("Pulling latest InvenTree image from docker hub (maybe grab a coffee!)") - + hide = None if debug else 'both' c.run("docker-compose -f test/docker-compose.yml pull", hide=hide) @@ -76,7 +76,7 @@ def check_server(c, host="http://localhost:12345", username="testuser", password except Exception as e: if debug: print("Error:", str(e)) - + if response is None: if timeout > 0: @@ -84,7 +84,7 @@ def check_server(c, host="http://localhost:12345", username="testuser", password print(f"No response from server. {timeout} seconds remaining") timeout -= 1 time.sleep(1) - + else: return False @@ -123,7 +123,7 @@ def start_server(c, debug=False): while not check_server(c, debug=False) and count > 0: count -= 1 time.sleep(1) - + if count == 0: print("No response from InvenTree server") sys.exit(1) @@ -160,7 +160,7 @@ def test(c, source=None, update=False, reset=False, debug=False): if not os.path.exists(source): source = os.path.join('test', source) - + if not os.path.exists(source): print(f"Error: Source file '{source}' does not exist") sys.exit(1) @@ -172,7 +172,7 @@ def test(c, source=None, update=False, reset=False, debug=False): if reset: stop_server(c, debug=debug) reset_data(c, debug=debug) - + # Launch the InvenTree server (in a docker container) start_server(c) diff --git a/test/test_api.py b/test/test_api.py index 9f8aca9d..243a000f 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- import os -import requests import sys import unittest +import requests + sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree import base # noqa: E402 from inventree import api # noqa: E402 +from inventree import base # noqa: E402 from inventree import part # noqa: E402 from inventree import stock # noqa: E402 - SERVER = os.environ.get('INVENTREE_PYTHON_TEST_SERVER', 'http://127.0.0.1:12345') USERNAME = os.environ.get('INVENTREE_PYTHON_TEST_USERNAME', 'testuser') PASSWORD = os.environ.get('INVENTREE_PYTHON_TEST_PASSWORD', 'testpassword') @@ -100,7 +100,7 @@ def test_read_parts(self): def test_file_download(self): """ - Attemtping to download a file while unauthenticated should raise an error + Attempting to download a file while unauthenticated should raise an error """ # Downloading without auth = unauthorized error (401) @@ -155,7 +155,7 @@ def test_details(self): self.assertIn('instance', details) self.assertIn('apiVersion', details) - + api_version = int(details['apiVersion']) self.assertTrue(api_version >= self.api.getMinApiVersion()) @@ -230,7 +230,7 @@ def test_get_widget(self): test_templates = widget.getTestTemplates() self.assertGreaterEqual(len(test_templates), 5) - + keys = [test.key for test in test_templates] self.assertIn('teststrengthofchair', keys) @@ -256,7 +256,7 @@ def test_add_template(self): self.assertEqual(len(widget.getTestTemplates()), n + 1) def test_add_result(self): - + # Look for a particular serial number items = stock.StockItem.list(self.api, serial=1000) self.assertEqual(len(items), 1) diff --git a/test/test_base.py b/test/test_base.py index 2a14467e..f59bc877 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -7,20 +7,21 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree.base import InventreeObject # noqa: E402 from test_api import InvenTreeTestCase # noqa: E402 +from inventree.base import InventreeObject # noqa: E402 + class BaseModelTests(InvenTreeTestCase): """Simple unit tests for the InvenTreeObject class""" - + def test_create(self): """Unit tests for InventreeObject creation""" # Test with non-int pk with self.assertRaises(TypeError): InventreeObject(None, pk='test') - + # Test with invalid pk for pk in [-1, 0]: with self.assertRaises(ValueError): @@ -34,7 +35,7 @@ def test_create(self): 'pk': 'seven', } ) - + def test_data_access(self): """Test data access functionality""" diff --git a/test/test_build.py b/test/test_build.py index 4c2825d1..5e9e8eaf 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -9,10 +9,10 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree.build import Build, BuildAttachment # noqa: E402 - from test_api import InvenTreeTestCase # noqa: E402 +from inventree.build import Build, BuildAttachment # noqa: E402 + class BuildOrderTest(InvenTreeTestCase): """ @@ -42,19 +42,19 @@ def get_build(self): ) else: build = builds[-1] - + return build def test_list_builds(self): - + build = self.get_build() - + self.assertIsNotNone(build) builds = Build.list(self.api) self.assertGreater(len(builds), 0) - + def test_build_attachment(self): """ Test that we can upload an attachment against a Build diff --git a/test/test_company.py b/test/test_company.py index 1d0d915c..61a6b775 100644 --- a/test/test_company.py +++ b/test/test_company.py @@ -12,14 +12,15 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) +from test_api import InvenTreeTestCase # noqa: E402 + from inventree import company # noqa: E402 from inventree.part import Part # noqa: E402 -from test_api import InvenTreeTestCase # noqa: E402 class ContactTest(InvenTreeTestCase): """Tests for the 'Contact' model""" - + def test_contact_create(self): """Test that we can create a new contact""" @@ -259,7 +260,7 @@ def test_upload_company_image(self): with self.assertRaises(FileNotFoundError): c.uploadImage('ddddummy_image.png') - + with self.assertRaises(TypeError): c.uploadImage(1) diff --git a/test/test_currency.py b/test/test_currency.py index 6792d9d5..fa37954b 100644 --- a/test/test_currency.py +++ b/test/test_currency.py @@ -22,7 +22,7 @@ def test_fetch_data(self): self.assertIsNotNone(mgr.base_currency) self.assertIsNotNone(mgr.exchange_rates) - + def test_base_currency(self): """Test call to 'getBaseCurrency'""" @@ -31,7 +31,7 @@ def test_base_currency(self): self.assertEqual(base, 'USD') self.assertIsNotNone(mgr.exchange_rates) - + def test_exchange_rates(self): """Test call to 'getExchangeRates'""" diff --git a/test/test_internal_price.py b/test/test_internal_price.py index abb57b33..e4d3af7d 100644 --- a/test/test_internal_price.py +++ b/test/test_internal_price.py @@ -5,13 +5,14 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree.part import Part, InternalPrice # noqa: E402 from test_api import InvenTreeTestCase # noqa: E402 +from inventree.part import InternalPrice, Part # noqa: E402 + class InternalPriceTest(InvenTreeTestCase): """ Test that the InternalPrice related objects can be managed via the API """ - + def test_fields(self): """ Test field names via OPTIONS request diff --git a/test/test_label.py b/test/test_label.py index 210c1a9d..0b6eeb01 100644 --- a/test/test_label.py +++ b/test/test_label.py @@ -6,7 +6,8 @@ sys.path.append(os.path.abspath(os.path.dirname(__file__))) from test_api import InvenTreeTestCase # noqa: E402 -from inventree.label import LabelPart, LabelStock, LabelLocation # noqa: E402 + +from inventree.label import LabelLocation, LabelPart, LabelStock # noqa: E402 from inventree.part import Part # noqa: E402 from inventree.stock import StockItem, StockLocation # noqa: E402 diff --git a/test/test_order.py b/test/test_order.py index 98e2aac9..e802923a 100644 --- a/test/test_order.py +++ b/test/test_order.py @@ -9,9 +9,9 @@ from test_api import InvenTreeTestCase # noqa: E402 -from inventree import part # noqa: E402 -from inventree import order # noqa: E402 from inventree import company # noqa: E402 +from inventree import order # noqa: E402 +from inventree import part # noqa: E402 from inventree import stock # noqa: E402 @@ -101,7 +101,7 @@ def test_po_create(self): if _order.pk == po.pk: found = True break - + self.assertTrue(found) # Should not be any line-items yet! @@ -123,7 +123,7 @@ def test_po_create(self): self.assertEqual(line.getOrder().pk, po.pk) self.assertIsNotNone(line) - + # Assert that a new line item has been created self.assertEqual(len(po.getLineItems()), idx) @@ -137,33 +137,33 @@ def test_po_create(self): self.assertEqual(line.quantity, idx + 1) self.assertEqual(line.received, 0) line.delete() - + # Assert length is now one less than before self.assertEqual(len(po.getLineItems()), 0) - + # Should not be any extra-line-items yet! extraitems = po.getExtraLineItems() self.assertEqual(len(extraitems), 0) - + # Let's add some! extraline = po.addExtraLineItem(quantity=1, reference="Transport costs", notes="Extra line item added from Python interface", price=10, price_currency="EUR") - + self.assertEqual(extraline.getOrder().pk, po.pk) self.assertIsNotNone(extraline) - + # Assert that a new line item has been created self.assertEqual(len(po.getExtraLineItems()), 1) - + # Assert that we can delete the item again extraline.delete() - + # Now there should be 0 lines left self.assertEqual(len(po.getExtraLineItems()), 0) def test_order_cancel(self): """Test that we can cancel a PurchaseOrder via the API""" - + n = len(order.PurchaseOrder.list(self.api)) + 1 ref = f"PO-{n}" @@ -230,7 +230,7 @@ def test_order_complete_with_receive(self): # Try to complete the order (should fail, as lines have not been received) with self.assertRaises(HTTPError): po.complete() - + po.reload() # Check that order status has *not* changed @@ -326,7 +326,7 @@ def test_order_complete(self): # Try to complete the order (should fail, as lines have not been received) with self.assertRaises(HTTPError): po.complete() - + po.reload() # Check that order status has *not* changed @@ -401,7 +401,7 @@ def test_so_fields(self): """ Check that the OPTIONS endpoint provides field names for this model """ - + names = [ 'customer', 'description', @@ -522,7 +522,7 @@ def test_so_attachment(self): pk = response['pk'] attachment = order.SalesOrderAttachment(self.api, pk=pk) - + self.assertEqual(attachment.order, so.pk) self.assertEqual(attachment.comment, 'Sales order attachment') @@ -540,7 +540,7 @@ def test_so_shipment(self): # Add some line items to the SalesOrder for p in part.Part.list(self.api, is_template=False, salable=True, limit=5): - + # Create a line item matching the part order.SalesOrderLineItem.create( self.api, @@ -642,7 +642,7 @@ def test_so_shipment(self): # Remember for later test allocated_quantities = dict() - + # Assign each line item to this shipment for si in so.getLineItems(): # If there is no stock available, delete this line @@ -688,7 +688,7 @@ def test_so_shipment(self): def test_order_cancel(self): """Test cancel sales order""" - + so = order.SalesOrder.create(self.api, { 'customer': 4, "description": "Selling some more stuff", @@ -720,7 +720,7 @@ def test_ro_fields(self): for name in names: self.assertIn(name, field_names) - + def test_ro_create(self): """Test that we can create a ReturnOrder""" @@ -753,7 +753,7 @@ def test_ro_create(self): reference=f"ref {idx}", notes="my notes", ) - + self.assertEqual(len(ro.getExtraLineItems()), 3) for line in ro.getExtraLineItems(): @@ -762,7 +762,7 @@ def test_ro_create(self): self.assertEqual(ro.getCustomer().pk, 4) self.assertIsNone(ro.getContact()) - + # Test that we can "edit" the order self.assertEqual(ro.reference, ref) @@ -779,10 +779,10 @@ def test_ro_create(self): # Should throw an error, as it no longer exists with self.assertRaises(HTTPError): ro.reload() - + def test_ro_cancel(self): """Test that an order can be cancelled""" - + ro = order.ReturnOrder.create(self.api, data={ 'description': 'To be cancelled', 'customer': 4, @@ -793,10 +793,10 @@ def test_ro_cancel(self): ro.reload() # Order should now be 'cancelled' self.assertEqual(ro.status, 40) - + def test_ro_issue(self): """Test that an order can be issued""" - + ro = order.ReturnOrder.create(self.api, data={ 'description': 'To be issued', 'customer': 4, diff --git a/test/test_part.py b/test/test_part.py index 283171b5..39eb89da 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import os -import requests import sys +import requests from requests.exceptions import HTTPError try: @@ -16,9 +16,12 @@ from test_api import InvenTreeTestCase # noqa: E402 from inventree.company import SupplierPart # noqa: E402 -from inventree.stock import StockItem # noqa: E402 -from inventree.part import Part, PartAttachment, PartCategory, PartCategoryParameterTemplate, Parameter, ParameterTemplate, PartTestTemplate, PartRelated, BomItem # noqa: E402 from inventree.part import InternalPrice # noqa: E402 +from inventree.part import (BomItem, Parameter, # noqa: E402 + ParameterTemplate, Part, PartAttachment, + PartCategory, PartCategoryParameterTemplate, + PartRelated, PartTestTemplate) +from inventree.stock import StockItem # noqa: E402 class PartCategoryTest(InvenTreeTestCase): @@ -55,11 +58,11 @@ def test_elec(self): for child in children: self.assertEqual(child.parent, 1) - + child = PartCategory(self.api, pk=3) self.assertEqual(child.name, 'Capacitors') self.assertEqual(child.getParentCategory().pk, electronics.pk) - + # Grab all child categories children = PartCategory.list(self.api, parent=child.pk) @@ -92,7 +95,7 @@ def test_elec(self): # Number of children should have increased! self.assertEqual(len(child.getChildCategories()), n + 3) - + def test_caps(self): cat = PartCategory(self.api, 6) @@ -118,7 +121,7 @@ def test_caps(self): self.assertIsNotNone(prt) self.assertEqual(prt.name, name) - + parts = cat.getParts() self.assertEqual(len(parts), n_parts + 10) @@ -157,11 +160,11 @@ def test_part_category_parameter_templates(self): self.assertTrue(len(templates) >= 3) # Check child categories - childs = electronics.getChildCategories() + children = electronics.getChildCategories() - self.assertTrue(len(childs) > 0) + self.assertTrue(len(children) > 0) - for child in childs: + for child in children: child_templates = child.getCategoryParameterTemplates(fetch_parent=True) self.assertTrue(len(child_templates) >= 3) @@ -204,7 +207,7 @@ def test_access_erors(self): with self.assertRaises(TypeError): Part(self.api, 'hello') - + with self.assertRaises(ValueError): Part(self.api, -1) @@ -368,7 +371,7 @@ def test_part_delete(self): """ Test we can create and delete a Part instance via the API """ - + n = len(Part.list(self.api)) # Create a new part @@ -418,7 +421,7 @@ def test_image_upload(self): # Attempt to upload an image with self.assertRaises(requests.exceptions.HTTPError): response = p.uploadImage("dummy_image.jpg") - + # Now, let's actually upload a real image img = Image.new('RGB', (128, 128), color='red') img.save('dummy_image.png') @@ -481,7 +484,7 @@ def test_part_attachment(self): ) self.assertIsNotNone(response) - + pk = response['pk'] # Check that a new attachment has been created! @@ -513,7 +516,7 @@ def test_set_price(self): # Grab all internal prices for the part ip = InternalPrice.list(self.api, part=p.pk) - # Delete any existsing prices + # Delete any existing prices for price in ip: self.assertEqual(type(price), InternalPrice) price.delete() @@ -528,7 +531,7 @@ def test_set_price(self): # Ensure that the part has an internal price ip = InternalPrice.list(self.api, part=p.pk) self.assertEqual(len(ip), 1) - + # Grab the internal price ip = ip[0] @@ -541,37 +544,37 @@ def test_parameters(self): """ Test setting and getting Part parameter templates, as well as parameter values """ - + # Count number of existing Parameter Templates existingTemplates = len(ParameterTemplate.list(self.api)) - + # Create new parameter template - this will fail, no name given with self.assertRaises(HTTPError): parametertemplate = ParameterTemplate.create(self.api, data={'units': "kg A"}) - + # Now create a proper parameter template parametertemplate = ParameterTemplate.create(self.api, data={'name': f'Test parameter no {existingTemplates}', 'units': "kg A"}) - + # result should not be None self.assertIsNotNone(parametertemplate) - + # Count should be one higher now self.assertEqual(len(ParameterTemplate.list(self.api)), existingTemplates + 1) - + # Grab the first part p = Part.list(self.api)[0] - + # Count number of parameters existingParameters = len(p.getParameters()) - + # Define parameter value for this part - without all required values with self.assertRaises(HTTPError): Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk}) - + # Define parameter value for this part - without all required values with self.assertRaises(HTTPError): Parameter.create(self.api, data={'part': p.pk, 'data': 10}) - + # Define w. required values - integer param = Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk, 'data': 10}) @@ -580,24 +583,24 @@ def test_parameters(self): # result should not be None self.assertIsNotNone(param) - + # Same parameter for same part - should fail # Define w. required values - string with self.assertRaises(HTTPError): Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk, 'data': 'String value'}) - + # Number of parameters should be one higher than before self.assertEqual(len(p.getParameters()), existingParameters + 1) - + # Delete the parameter param.delete() - + # Check count self.assertEqual(len(p.getParameters()), existingParameters) - + # Delete the parameter template parametertemplate.delete() - + # Check count self.assertEqual(len(ParameterTemplate.list(self.api)), existingTemplates) @@ -703,7 +706,7 @@ def test_barcode_assign(self): self.assertEqual(response['success'], 'Assigned barcode to part instance') self.assertEqual(response['barcode_data'], barcode) - + # Attempt to assign the same barcode to a different part (should error) part_2 = Part(self.api, pk=2) @@ -725,7 +728,7 @@ def test_barcode_assign(self): # Now assign to part_2 response = part_2.assignBarcode(barcode) self.assertEqual(response['barcode_data'], barcode) - + # Scan again response = self.api.scanBarcode(barcode) self.assertEqual(response['part']['pk'], 2) @@ -740,7 +743,7 @@ def test_barcode_assign(self): class PartTestTemplateTest(InvenTreeTestCase): """Tests for PartTestTemplate functionality""" - + def test_generateKey(self): """Tests for generating a key for a PartTestTemplate""" diff --git a/test/test_project_codes.py b/test/test_project_codes.py index b0da9c1b..a831c04e 100644 --- a/test/test_project_codes.py +++ b/test/test_project_codes.py @@ -2,18 +2,20 @@ import os import sys + sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree.project_code import ProjectCode # noqa: E402 from test_api import InvenTreeTestCase # noqa: E402 +from inventree.project_code import ProjectCode # noqa: E402 + class ProjectCodeTest(InvenTreeTestCase): """Tests for the ProjectCode model""" def test_project_code_create(self): """Test we can create a new project code""" - + n = ProjectCode.count(self.api) ProjectCode.create(self.api, { @@ -38,7 +40,7 @@ def test_project_code_create(self): 'code': f'CODE-{idx + n}', 'description': f'Description {idx + n}', }) - + # List all codes codes = ProjectCode.list(self.api) diff --git a/test/test_stock.py b/test/test_stock.py index eac3b5e6..360b5c74 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- import os -import requests import sys +import requests sys.path.append(os.path.abspath(os.path.dirname(__file__))) -from inventree.stock import StockItem, StockLocation # noqa: E402 -from inventree import part # noqa: E402 -from inventree import company # noqa: E402 - from test_api import InvenTreeTestCase # noqa: E402 +from inventree import company # noqa: E402 +from inventree import part # noqa: E402 +from inventree.stock import StockItem, StockLocation # noqa: E402 + class StockLocationTest(InvenTreeTestCase): """ Tests for the StockLocation model Fixture data can be found in the InvenTree source: - + - InvenTree/stock/fixtures/location.yaml """ @@ -28,7 +28,7 @@ def test_location_list(self): """ Test the LIST API endpoint for the StockLocation model """ - + locs = StockLocation.list(self.api) self.assertGreaterEqual(len(locs), 4) @@ -103,7 +103,7 @@ def test_location_stock(self): "location": location.pk, } ) - + items = location.getStockItems(part=1) self.assertEqual(len(items), n + i + 1) @@ -151,7 +151,7 @@ class StockTest(InvenTreeTestCase): Test alternative ways of getting StockItem objects. Fixture data can be found in the InvenTree source: - + - InvenTree/stock/fixtures/stock.yaml """ @@ -169,11 +169,11 @@ def test_stock(self): # Request via the Part instance (results should be the same!) items = part.Part(self.api, 1).getStockItems() self.assertEqual(len(items), n) - + def test_get_stock_item(self): """ StockItem API tests. - + Refer to fixture data in InvenTree/stock/fixtures/stock.yaml """ @@ -188,7 +188,7 @@ def test_get_stock_item(self): # Move the item to a known location item.transferStock(3) item.reload() - + location = item.getLocation() self.assertEqual(type(location), StockLocation) @@ -285,7 +285,7 @@ def test_count(self): # Check error conditions with self.assertRaises(requests.exceptions.HTTPError): item.countStock('not a number') - + with self.assertRaises(requests.exceptions.HTTPError): item.countStock(-1) @@ -345,7 +345,7 @@ def test_transfer(self): for loc in [-1, 'qqq', 99999, None]: with self.assertRaises(requests.exceptions.HTTPError): item.transferStock(loc) - + # Attempt to transfer with an invalid quantity for q in [-1, None, 'hhhh']: with self.assertRaises(requests.exceptions.HTTPError):