Compare commits

...

6 commits

Author SHA1 Message Date
Jan Böhmer
03e1105a8e Fixed phpstan issues
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
2025-03-27 23:11:49 +01:00
Jan Böhmer
059a9683db Fixed problem that global_theme setting was not respected
This fixes issue #880
2025-03-27 21:47:52 +01:00
Jan Böhmer
1daf6f01f4 Fixed error 500 if internal attachment path was not resolvable to an URL
This fixes issue #898
2025-03-27 21:40:51 +01:00
Daniel Carrasco
d3b225771c
Modified the DigiKey Provider to works with the V4 API (#875)
* Modified the DigiKey Provider to works with the V4 API

* Correclty apply the MarketPlaceFilter option to digikey v4 API

* Show the packe type (Tape&Reel, Box, etc.) as footprint in digikey provider search

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-03-27 21:26:18 +01:00
Jan Böhmer
7275db27e7 Manually filter mouser search results to fix the edgecase, that the API returned multiple results for an exact part number
This fixes issue #888 and issue #616
2025-03-27 21:06:50 +01:00
Jan Böhmer
49ee9131d0 Use composer/ca-bundle instead of system CA for element14 provider
This is a workaround for debian systems, where the required root CA is missing as trusted CA in the system CAs. This fixes issue #891 and #866
2025-03-27 20:59:22 +01:00
7 changed files with 103 additions and 55 deletions

View file

@ -15,7 +15,7 @@
"api-platform/core": "^3.1",
"beberlei/doctrineextensions": "^1.2",
"brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.3",
"composer/ca-bundle": "^1.5",
"composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/data-fixtures": "^2.0.0",
"doctrine/dbal": "^4.0.0",

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "75643d42e05fce4684644d375bff2d0a",
"content-hash": "7fb73581b0074c5a79afb3ffa614ed8e",
"packages": [
{
"name": "amphp/amp",

View file

@ -156,8 +156,10 @@ class EntityURLGenerator
public function viewURL(Attachment $entity): string
{
if ($entity->hasInternal()) {
return $this->attachmentURLGenerator->getInternalViewURL($entity);
//If the underlying file path is invalid, null gets returned, which is not allowed here.
//We still have the chance to use an external path, if it is set.
if ($entity->hasInternal() && ($url = $this->attachmentURLGenerator->getInternalViewURL($entity)) !== null) {
return $url;
}
if($entity->hasExternal()) {

View file

@ -108,12 +108,15 @@ class DigikeyProvider implements InfoProviderInterface
{
$request = [
'Keywords' => $keyword,
'RecordCount' => 50,
'RecordStartPosition' => 0,
'ExcludeMarketPlaceProducts' => 'true',
'Limit' => 50,
'Offset' => 0,
'FilterOptionsRequest' => [
'MarketPlaceFilter' => 'ExcludeMarketPlace',
],
];
$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
//$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
$response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [
'json' => $request,
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
]);
@ -124,18 +127,21 @@ class DigikeyProvider implements InfoProviderInterface
$result = [];
$products = $response_array['Products'];
foreach ($products as $product) {
$result[] = new SearchResultDTO(
provider_key: $this->getProviderKey(),
provider_id: $product['DigiKeyPartNumber'],
name: $product['ManufacturerPartNumber'],
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
category: $this->getCategoryString($product),
manufacturer: $product['Manufacturer']['Value'] ?? null,
mpn: $product['ManufacturerPartNumber'],
preview_image_url: $product['PrimaryPhoto'] ?? null,
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
provider_url: $product['ProductUrl'],
);
foreach ($product['ProductVariations'] as $variation) {
$result[] = new SearchResultDTO(
provider_key: $this->getProviderKey(),
provider_id: $variation['DigiKeyProductNumber'],
name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
category: $this->getCategoryString($product),
manufacturer: $product['Manufacturer']['Name'] ?? null,
mpn: $product['ManufacturerProductNumber'],
preview_image_url: $product['PhotoUrl'] ?? null,
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
provider_url: $product['ProductUrl'],
footprint: $variation['PackageType']['Name'], //Use the footprint field, to show the user the package type (Tape & Reel, etc., as digikey has many different package types)
);
}
}
return $result;
@ -143,62 +149,79 @@ class DigikeyProvider implements InfoProviderInterface
public function getDetails(string $id): PartDetailDTO
{
$response = $this->digikeyClient->request('GET', '/Search/v3/Products/' . urlencode($id), [
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
]);
$product = $response->toArray();
$response_array = $response->toArray();
$product = $response_array['Product'];
$footprint = null;
$parameters = $this->parametersToDTOs($product['Parameters'] ?? [], $footprint);
$media = $this->mediaToDTOs($product['MediaLinks']);
$media = $this->mediaToDTOs($id);
// Get the price_breaks of the selected variation
$price_breaks = [];
foreach ($product['ProductVariations'] as $variation) {
if ($variation['DigiKeyProductNumber'] == $id) {
$price_breaks = $variation['StandardPricing'] ?? [];
break;
}
}
return new PartDetailDTO(
provider_key: $this->getProviderKey(),
provider_id: $product['DigiKeyPartNumber'],
name: $product['ManufacturerPartNumber'],
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
provider_id: $id,
name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
category: $this->getCategoryString($product),
manufacturer: $product['Manufacturer']['Value'] ?? null,
mpn: $product['ManufacturerPartNumber'],
preview_image_url: $product['PrimaryPhoto'] ?? null,
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
manufacturer: $product['Manufacturer']['Name'] ?? null,
mpn: $product['ManufacturerProductNumber'],
preview_image_url: $product['PhotoUrl'] ?? null,
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
provider_url: $product['ProductUrl'],
footprint: $footprint,
datasheets: $media['datasheets'],
images: $media['images'],
parameters: $parameters,
vendor_infos: $this->pricingToDTOs($product['StandardPricing'] ?? [], $product['DigiKeyPartNumber'], $product['ProductUrl']),
vendor_infos: $this->pricingToDTOs($price_breaks, $id, $product['ProductUrl']),
);
}
/**
* Converts the product status from the Digikey API to the manufacturing status used in Part-DB
* @param string|null $dk_status
* @param int|null $dk_status
* @return ManufacturingStatus|null
*/
private function productStatusToManufacturingStatus(?string $dk_status): ?ManufacturingStatus
private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus
{
// The V4 can use strings to get the status, but if you have changed the PROVIDER_DIGIKEY_LANGUAGE it will not match.
// Using the Id instead which should be fixed.
//
// The API is not well documented and the ID are not there yet, so were extracted using "trial and error".
// The 'Preliminary' id was not found in several categories so I was unable to extract it. Disabled for now.
return match ($dk_status) {
null => null,
'Active' => ManufacturingStatus::ACTIVE,
'Obsolete' => ManufacturingStatus::DISCONTINUED,
'Discontinued at Digi-Key', 'Last Time Buy' => ManufacturingStatus::EOL,
'Not For New Designs' => ManufacturingStatus::NRFND,
'Preliminary' => ManufacturingStatus::ANNOUNCED,
0 => ManufacturingStatus::ACTIVE,
1 => ManufacturingStatus::DISCONTINUED,
2, 4 => ManufacturingStatus::EOL,
7 => ManufacturingStatus::NRFND,
//'Preliminary' => ManufacturingStatus::ANNOUNCED,
default => ManufacturingStatus::NOT_SET,
};
}
private function getCategoryString(array $product): string
{
$category = $product['Category']['Value'];
$sub_category = $product['Family']['Value'];
$category = $product['Category']['Name'];
$sub_category = current($product['Category']['ChildCategories']);
//Replace the ' - ' category separator with ' -> '
$sub_category = str_replace(' - ', ' -> ', $sub_category);
if ($sub_category) {
//Replace the ' - ' category separator with ' -> '
$category = $category . ' -> ' . str_replace(' - ', ' -> ', $sub_category["Name"]);
}
return $category . ' -> ' . $sub_category;
return $category;
}
/**
@ -215,18 +238,18 @@ class DigikeyProvider implements InfoProviderInterface
foreach ($parameters as $parameter) {
if ($parameter['ParameterId'] === 1291) { //Meaning "Manufacturer given footprint"
$footprint_name = $parameter['Value'];
$footprint_name = $parameter['ValueText'];
}
if (in_array(trim((string) $parameter['Value']), ['', '-'], true)) {
if (in_array(trim((string) $parameter['ValueText']), ['', '-'], true)) {
continue;
}
//If the parameter was marked as text only, then we do not try to parse it as a numerical value
if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) {
$results[] = new ParameterDTO(name: $parameter['Parameter'], value_text: $parameter['Value']);
$results[] = new ParameterDTO(name: $parameter['ParameterText'], value_text: $parameter['ValueText']);
} else { //Otherwise try to parse it as a numerical value
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterText'], $parameter['ValueText']);
}
}
@ -254,16 +277,22 @@ class DigikeyProvider implements InfoProviderInterface
}
/**
* @param array $media_links
* @param string $id The Digikey product number, to get the media for
* @return FileDTO[][]
* @phpstan-return array<string, FileDTO[]>
*/
private function mediaToDTOs(array $media_links): array
private function mediaToDTOs(string $id): array
{
$datasheets = [];
$images = [];
foreach ($media_links as $media_link) {
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/media', [
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
]);
$media_array = $response->toArray();
foreach ($media_array['MediaLinks'] as $media_link) {
$file = new FileDTO(url: $media_link['Url'], name: $media_link['Title']);
switch ($media_link['MediaType']) {

View file

@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use Composer\CaBundle\CaBundle;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Element14Provider implements InfoProviderInterface
@ -43,9 +44,19 @@ class Element14Provider implements InfoProviderInterface
private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant',
'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode'];
public function __construct(private readonly HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id)
{
private readonly HttpClientInterface $element14Client;
public function __construct(HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id)
{
/* We use the mozilla CA from the composer ca bundle directly, as some debian systems seems to have problems
* with the SSL.COM CA, element14 uses. See https://github.com/Part-DB/Part-DB-server/issues/866
*
* This is a workaround until the issue is resolved in debian (or never).
* As this only affects this provider, this should have no negative impact and the CA bundle is still secure.
*/
$this->element14Client = $element14Client->withOptions([
'cafile' => CaBundle::getBundledCaBundlePath(),
]);
}
public function getProviderInfo(): array

View file

@ -94,6 +94,7 @@ class MouserProvider implements InfoProviderInterface
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.
@ -176,11 +177,16 @@ class MouserProvider implements InfoProviderInterface
throw new \RuntimeException('No part found with ID '.$id);
}
//Manually filter out the part with the correct ID
$tmp = array_filter($tmp, fn(PartDetailDTO $part) => $part->provider_id === $id);
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 . ' ('.count($tmp).' found). This is basically a bug in Mousers API response. See issue #616.');
throw new \RuntimeException('Multiple parts found with ID '.$id);
}
return $tmp[0];
return reset($tmp);
}
public function getCapabilities(): array

View file

@ -38,7 +38,7 @@
{% if not app.user.theme is defined %}
{% if not app.user.theme is defined or app.user.theme is null %}
{% set theme = global_theme %}
{% else %}
{% set theme = app.user.theme %}