Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

All notable changes to SmartParts are documented in this file.

## [1.1.0] — 2026-05-31

### Added
- **Dedicated Parameter Normalization Dashboard**: Built a separate Django View and HTML Template specifically for the Parameter Normalization Dashboard, moving management cleanly out of cramped plugin settings.
- **Permanent Ignore Filter Heuristics**: Added permanent ignore dropdown support. Ignored parameters are saved with `is_ignored = True` and silently dropped during the data merge phase in `data_merger.py` (and Creator safeguards).
- **Autocomplete Dynamic Dropdowns**: Integrated autocomplete inputs for Canonical Values utilizing the database-backed `ParameterTemplate` options combined with hardcoded canonical electronic maps.
- **TME.eu API Client**: Secure, HMAC-SHA1 signed API integration to retrieve TME pricing, stock, and parameters.
- **element14 / Farnell API Client**: regional storefront selection with fallback lookup capabilities.
- **Regex-Based Parameter Sanitization**: String pre-processing filter to strip punctuation (hyphens, underscores, brackets, parentheses, slashes) and normalize whitespaces for robust canonical matches.
- **"Catch & Learn" Parameter Map UI**: Interactive Dashboard interface to track unknown parameters returned from APIs and map them dynamically.
- **GS character Wedge Handling**: Parser support for high-density DataMatrix barcodes containing non-printable GS (ASCII 29) group separator characters from hardware keyboard wedge scanners.
- **CI/CD Integration**: Configured GitHub Actions workflows for automated PEP 8 lints (`flake8`), `black` code styling checks, and full normalizer test execution.

### Changed
- **Parameter Normalization Expansion**: Expanded standard `PARAMETER_MAP` to cover 100+ common electronic, mechanical, and semiconductor units with SI scaling (e.g. converting `0.00001 F` -> `10 µF`).
- **Codebase Sanitization & Formatting**: Reformatted the entire Python codebase (22 modules) using the `black` formatter to strictly conform to PEP 8.
- **Documentation Overhaul**: Rewrote the entire `README.md` to showcase the new multi-source features, parameter mapping architecture, PureScan terminal commands, and configuration variables.

### Fixed
- **Sanitized API Diagnostics & Logs**: Uniformly standardized all connection test results to return clean, standard success formats (e.g. `Connected successfully. Test search returned: LM7805`) and implemented a robust query parameter filter to scrub sensitive parameters/URLs from all API error exceptions and activity log warnings.
- **Dead Code & Unused Imports Cleanup**: Removed unused imports and legacy variables across all Python files, including `duplicate_checker.py`, `image_handler.py`, `views.py`, and `tools/generate_command_sheet.py`.
- **Unicode Console Output**: Resolved cp1252 encoding and character crashes on Windows terminal environments when testing values containing Ω or µ symbols.

## [1.0.1] — 2026-05-16

### Fixed
Expand Down Expand Up @@ -30,4 +53,4 @@ All notable changes to SmartParts are documented in this file.
- Auto label printing (Dymo/Zebra via inventreelabelmachine)
- Duplicate detection with update-in-place support
- DataMatrix barcode parsing (ANSI MH10.8.2 / ISO/IEC 15434)
- PUI integration (panels + dashboard widgets)
- PUI integration (panels + dashboard widgets)
7 changes: 6 additions & 1 deletion inventree_smart_parts/api_clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""
API Client Layer
================
Provides uniform access to distributor APIs (Mouser, DigiKey, LCSC).
Provides uniform access to distributor APIs (Mouser, DigiKey, LCSC,
element14/Farnell, TME).
Each client normalizes response data into a shared PartData structure.
"""

from .base import BaseApiClient, PartData, PriceBreak, PartParameter
from .mouser import MouserClient
from .digikey import DigiKeyClient
from .lcsc import LCSCClient
from .element14 import Element14Client
from .tme import TMEClient

__all__ = [
"BaseApiClient",
Expand All @@ -18,4 +21,6 @@
"MouserClient",
"DigiKeyClient",
"LCSCClient",
"Element14Client",
"TMEClient",
]
29 changes: 23 additions & 6 deletions inventree_smart_parts/api_clients/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
logger = logging.getLogger("inventree_smart_parts.api")


def sanitize_error_message(msg: str) -> str:
"""Strip query parameters from URLs inside the error message to prevent credential/parameter leaks."""
if not msg:
return msg
import re

return re.sub(r"\?[^\s'\"]*", "", msg)


# ═══════════════════════════════════════════════════════════════════
# Shared Data Structures
# ═══════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -192,25 +201,33 @@ def _request(
return data

except requests.exceptions.Timeout:
logger.error(f"[{self.SOURCE_NAME}] Request timeout for URL: {url}")
cleaned_url = url.split("?")[0]
logger.error(f"[{self.SOURCE_NAME}] Request timeout for URL: {cleaned_url}")
raise ApiTimeoutError(f"{self.SOURCE_NAME} API request timed out")

except requests.exceptions.HTTPError as e:
status = e.response.status_code if e.response is not None else "unknown"
logger.error(f"[{self.SOURCE_NAME}] HTTP {status} error: {e}")
raw_err_msg = str(e)
cleaned_msg = sanitize_error_message(raw_err_msg)
logger.error(f"[{self.SOURCE_NAME}] HTTP {status} error: {cleaned_msg}")
raise ApiHttpError(
f"{self.SOURCE_NAME} API returned HTTP {status}",
f"{self.SOURCE_NAME} API returned HTTP {status}: {cleaned_msg}",
status_code=status,
response=e.response,
)

except requests.exceptions.ConnectionError:
logger.error(f"[{self.SOURCE_NAME}] Connection failed for URL: {url}")
cleaned_url = url.split("?")[0]
logger.error(
f"[{self.SOURCE_NAME}] Connection failed for URL: {cleaned_url}"
)
raise ApiConnectionError(f"Could not connect to {self.SOURCE_NAME} API")

except requests.exceptions.RequestException as e:
logger.error(f"[{self.SOURCE_NAME}] Request error: {e}")
raise ApiError(f"{self.SOURCE_NAME} API error: {e}")
raw_err_msg = str(e)
cleaned_msg = sanitize_error_message(raw_err_msg)
logger.error(f"[{self.SOURCE_NAME}] Request error: {cleaned_msg}")
raise ApiError(f"{self.SOURCE_NAME} API error: {cleaned_msg}")

@abstractmethod
def search_by_mpn(self, mpn: str) -> Optional[PartData]:
Expand Down
10 changes: 7 additions & 3 deletions inventree_smart_parts/api_clients/digikey.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,19 +342,23 @@ def test_connection(self) -> Dict[str, Any]:
self._authenticate()
return {
"success": True,
"message": "OAuth2 authentication successful. Connection OK.",
"message": "Connected successfully. OAuth2 authentication OK.",
"details": {
"token_expires_in": int(self._token_expiry - time.time()),
},
}

except ApiAuthError as e:
from .base import sanitize_error_message

return {
"success": False,
"message": f"Authentication failed: {str(e)}",
"message": f"Authentication failed: {sanitize_error_message(str(e))}",
}
except Exception as e:
from .base import sanitize_error_message

return {
"success": False,
"message": f"Unexpected error: {str(e)}",
"message": f"Unexpected error: {sanitize_error_message(str(e))}",
}
Loading
Loading