Merge branch 'mouser_pdo59'

This resolve issue #329
This commit is contained in:
Jan Böhmer 2023-10-08 00:40:26 +02:00
commit 1964084155
5 changed files with 336 additions and 0 deletions

View file

@ -41,6 +41,7 @@
PassEnv PROVIDER_ELEMENT14_KEY PROVIDER_ELEMENT14_STORE_ID
PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to

11
.env
View file

@ -142,6 +142,17 @@ PROVIDER_OCTOPART_SEARCH_LIMIT=10
# Set to false to include non authorized offers in the results
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
# Mouser Provider API V2:
# You can get your API key from https://www.mouser.it/api-hub/
PROVIDER_MOUSER_KEY=
# Filter search results by RoHS compliance and stock availability:
# Available options: None | Rohs | InStock | RohsAndInStock
PROVIDER_MOUSER_SEARCH_OPTION='None'
# The number of results to get from Mouser while searching (please note that this value is max 50)
PROVIDER_MOUSER_SEARCH_LIMIT=50
# It is recommended to leave this set to 'true'. The option is not really good doumented by Mouser:
# Used when searching for keywords in the language specified when you signed up for Search API.
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
###################################################################################
# SAML Single sign on-settings

View file

@ -277,6 +277,13 @@ services:
$search_limit: '%env(int:PROVIDER_OCTOPART_SEARCH_LIMIT)%'
$onlyAuthorizedSellers: '%env(bool:PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS)%'
App\Services\InfoProviderSystem\Providers\MouserProvider:
arguments:
$api_key: '%env(string:PROVIDER_MOUSER_KEY)%'
$language: '%env(string:PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE)%'
$options: '%env(string:PROVIDER_MOUSER_SEARCH_OPTION)%'
$search_limit: '%env(int:PROVIDER_MOUSER_SEARCH_LIMIT)%'
####################################################################################################################
# API system
####################################################################################################################

View file

@ -126,6 +126,18 @@ Following env configuration options are available:
* `PROVIDER_ELEMENT14_KEY`: The API key you got from Farnell (mandatory)
* `PROVIDER_ELEMENT14_STORE_ID`: The store ID you want to use. This decides the language of results, currency and country of prices (optional, default: `de.farnell.com`, see [here](https://partner.element14.com/docs/Product_Search_API_REST__Description) for availailable values)
### Mouser
The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and getting shopping information from [Mouser](https://www.mouser.com/).
You have to create an account at Mouser and register for an API key for the Search API on the [Mouser API page](https://www.mouser.de/api-home/).
You will receive an API token, which you have to enter in the Part-DB env configuration (see below):
At the registration you choose a country, language and currency in which you want to get the results.
Following env configuration options are available:
* `PROVIDER_MOUSER_KEY`: The API key you got from Mouser (mandatory)
* `PROVIDER_MOUSER_SEARCH_LIMIT`: The maximum number of results to return per search (maximum 50)
* `PROVIDER_MOUSER_SEARCH_OPTION`: You can choose an option here to restrict the search results to RoHs compliant and available parts. Possible values are `None`, `Rohs`, `InStock`, `RohsAndInStock`.
* `PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE`: A bit of an obscure option. The original description of Mouser is: Used when searching for keywords in the language specified when you signed up for Search API.
### Custom provider
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long as it is a valid Symfony service, it will be automatically loaded and can be used.

View file

@ -0,0 +1,305 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This file provide an interface with the Mouser API V2 (also compatible with the V1)
*
* Copyright (C) 2023 Pasquale D'Orsi (https://github.com/pdo59)
*
* TODO: Obtain an API keys with an US Mouser user (currency $) and test the result of prices
*
*/
declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers;
use App\Entity\Parts\ManufacturingStatus;
use App\Form\InfoProviderSystem\ProviderSelectType;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class MouserProvider implements InfoProviderInterface
{
private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search';
public const DISTRIBUTOR_NAME = 'Mouser';
public function __construct(
private readonly HttpClientInterface $mouserClient,
private readonly string $api_key,
private readonly string $language,
private readonly string $options,
private readonly int $search_limit
) {
}
public function getProviderInfo(): array
{
return [
'name' => 'Mouser',
'description' => 'This provider uses the Mouser API to search for parts.',
'url' => 'https://www.mouser.com/',
'disabled_help' => 'Configure the API key in the PROVIDER_MOUSER_KEY environment variable to enable.'
];
}
public function getProviderKey(): string
{
return 'mouser';
}
public function isActive(): bool
{
return !empty($this->api_key);
}
public function searchByKeyword(string $keyword): array
{
/*
SearchByKeywordRequest description:
Search parts by keyword and return a maximum of 50 parts.
keyword* string
Used for keyword part search.
records integer($int32)
Used to specify how many records the method should return.
startingRecord integer($int32)
Indicates where in the total recordset the return set should begin.
From the startingRecord, the number of records specified will be returned up to the end of the recordset.
This is useful for paging through the complete recordset of parts matching keyword.
searchOptions string
Optional.
If not provided, the default is None.
Refers to options supported by the search engine.
Only one value at a time is supported.
Available options: None | Rohs | InStock | RohsAndInStock - can use string representations or integer IDs: 1[None] | 2[Rohs] | 4[InStock] | 8[RohsAndInStock].
searchWithYourSignUpLanguage string
Optional.
If not provided, the default is false.
Used when searching for keywords in the language specified when you signed up for Search API.
Can use string representation: true.
{
"SearchByKeywordRequest": {
"keyword": "BC557",
"records": 0,
"startingRecord": 0,
"searchOptions": "",
"searchWithYourSignUpLanguage": ""
}
}
*/
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/keyword", [
'query' => [
'apiKey' => $this->api_key,
],
'json' => [
'SearchByKeywordRequest' => [
'keyword' => $keyword,
'records' => $this->search_limit, //self::NUMBER_OF_RESULTS,
'startingRecord' => 0,
'searchOptions' => $this->options,
'searchWithYourSignUpLanguage' => $this->language,
]
],
]);
return $this->responseToDTOArray($response);
}
public function getDetails(string $id): PartDetailDTO
{
/*
SearchByPartRequest description:
Search parts by part number and return a maximum of 50 parts.
mouserPartNumber string
Used to search parts by the specific Mouser part number with a maximum input of 10 part numbers, separated by a pipe symbol for the search.
Each part number must be a minimum of 3 characters and a maximum of 40 characters. For example: 494-JANTX2N2222A|610-2N2222-TL|637-2N2222A
partSearchOptions string
Optional.
If not provided, the default is None. Refers to options supported by the search engine. Only one value at a time is supported.
The following values are valid: None | Exact - can use string representations or integer IDs: 1[None] | 2[Exact]
{
"SearchByPartRequest": {
"mouserPartNumber": "string",
"partSearchOptions": "string"
}
}
*/
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/partnumber", [
'query' => [
'apiKey' => $this->api_key,
],
'json' => [
'SearchByPartRequest' => [
'mouserPartNumber' => $id,
'partSearchOptions' => 2
]
],
]);
$tmp = $this->responseToDTOArray($response);
//Ensure that we have exactly one result
if (count($tmp) === 0) {
throw new \RuntimeException('No part found with ID '.$id);
}
if (count($tmp) > 1) {
throw new \RuntimeException('Multiple parts found with ID '.$id);
}
return $tmp[0];
}
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
/**
* @param ResponseInterface $response
* @return PartDetailDTO[]
*/
private function responseToDTOArray(ResponseInterface $response): array
{
$arr = $response->toArray();
if (isset($arr['SearchResults'])) {
$products = $arr['SearchResults']['Parts'] ?? [];
} else {
throw new \RuntimeException('Unknown response format');
}
$result = [];
foreach ($products as $product) {
$result[] = new PartDetailDTO(
provider_key: $this->getProviderKey(),
provider_id: $product['MouserPartNumber'],
name: $product['ManufacturerPartNumber'],
description: $product['Description'],
category: $product['Category'],
manufacturer: $product['Manufacturer'],
mpn: $product['ManufacturerPartNumber'],
preview_image_url: $product['ImagePath'],
manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['LifecycleStatus'] ?? null),
provider_url: $product['ProductDetailUrl'],
datasheets: $this->parseDataSheets($product['DataSheetUrl'] ?? null,
$product['MouserPartNumber'] ?? null),
vendor_infos: $this->pricingToDTOs($product['PriceBreaks'] ?? [], $product['MouserPartNumber'],
$product['ProductDetailUrl']),
);
}
return $result;
}
private function parseDataSheets(?string $sheetUrl, ?string $sheetName): ?array
{
if (empty($sheetUrl)) {
return null;
}
$result = [];
$result[] = new FileDTO(url: $sheetUrl, name: $sheetName);
return $result;
}
/*
* Mouser API price is a string in the form "n[.,]nnn[.,] currency"
* then this convert it to a number
*/
private function priceStrToFloat($val): float
{
$val = str_replace(",", ".", $val);
$val = preg_replace('/\.(?=.*\.)/', '', $val);
return (float)$val;
}
/**
* Converts the pricing (StandardPricing field) from the Mouser API to an array of PurchaseInfoDTOs
* @param array $price_breaks
* @param string $order_number
* @param string $product_url
* @return PurchaseInfoDTO[]
*/
private function pricingToDTOs(array $price_breaks, string $order_number, string $product_url): array
{
$prices = [];
foreach ($price_breaks as $price_break) {
$number = $this->priceStrToFloat($price_break['Price']);
$prices[] = new PriceDTO(
minimum_discount_amount: $price_break['Quantity'],
price: (string)$number,
currency_iso_code: $price_break['Currency']
);
}
return [
new PurchaseInfoDTO(distributor_name: self::DISTRIBUTOR_NAME, order_number: $order_number, prices: $prices,
product_url: $product_url)
];
}
/* Converts the product status from the MOUSER API to the manufacturing status used in Part-DB:
Factory Special Order - Ordine speciale in fabbrica
Not Recommended for New Designs - Non raccomandato per nuovi progetti
New Product - Nuovo prodotto
End of Life - Fine vita
-vuoto- - Attivo
TODO: Probably need to review the values of field Lifecyclestatus
*/
private function releaseStatusCodeToManufacturingStatus(?string $productStatus): ?ManufacturingStatus
{
return match ($productStatus) {
null => null,
"New Product" => ManufacturingStatus::ANNOUNCED,
"Not Recommended for New Designs" => ManufacturingStatus::NRFND,
"Factory Special Order" => ManufacturingStatus::DISCONTINUED,
"End of Life" => ManufacturingStatus::EOL,
"Obsolete" => ManufacturingStatus::DISCONTINUED,
default => ManufacturingStatus::ACTIVE,
};
}
}