Skip to content

updated to digikey v4 and python 3.12 #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ manufacturers overlap other manufacturer part numbers.
[![Donate](https://img.shields.io/badge/Donate-PayPal-gold.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=53HWHHVCJ3D4J&currency_code=EUR&source=url)

# What does it do
`digikey-api` is an [Digkey Part Search API](https://api-portal.digikey.com/node/8517) client for Python 3.6+. API response data is returned as Python objects that attempt to make it easy to get the data you want. Not all endpoints have been implemented.
`digikey-api` is an [Digkey Part Search API](https://api-portal.digikey.com/node/8517) client for Python 3.12+. API response data is returned as Python objects that attempt to make it easy to get the data you want. Not all endpoints have been implemented.

# Quickstart

Expand All @@ -25,15 +25,15 @@ export DIGIKEY_STORAGE_PATH="path/to/cache/dir"

The cache dir is used to store the OAUTH access and refresh token, if you delete it you will need to login again.

# API V3
# API V4
## Register
Register an app on the Digikey API portal: [Digi-Key API V3](https://developer.digikey.com/get_started). You will need
Register an app on the Digikey API portal: [Digi-Key API V4](https://developer.digikey.com/get_started). You will need
the client ID and the client secret to use the API. You will also need a Digi-Key account to authenticate, using the
Oauth2 process.

When registering an app the OAuth Callback needs to be set to `https://localhost:8139/digikey_callback`.

## Use [API V3]
## Use [API V4]
Python will automatically spawn a browser to allow you to authenticate using the Oauth2 process. After obtaining a token
the library will cache the access token and use the refresh token to automatically refresh your credentials.

Expand All @@ -48,8 +48,8 @@ import os
from pathlib import Path

import digikey
from digikey.v3.productinformation import KeywordSearchRequest
from digikey.v3.batchproductdetails import BatchProductDetailsRequest
from digikey.v4.productinformation import KeywordRequest
from digikey.v4.batchproductdetails import BatchProductDetailsRequest

CACHE_DIR = Path('path/to/cache/dir')

Expand All @@ -63,7 +63,7 @@ dkpn = '296-6501-1-ND'
part = digikey.product_details(dkpn)

# Search for parts
search_request = KeywordSearchRequest(keywords='CRCW080510K0FKEA', record_count=10)
search_request = KeywordSearchRequest(keywords='CRCW080510K0FKEA', limit=10, offset = 0)
result = digikey.keyword_search(body=search_request)

# Only if BatchProductDetails endpoint is explicitly enabled
Expand All @@ -73,7 +73,7 @@ batch_request = BatchProductDetailsRequest(products=mpn_list)
part_results = digikey.batch_product_details(body=batch_request)
```

## Logging [API V3]
## Logging [API V4]
Logging is not forced upon the user but can be enabled according to convention:
```python
import logging
Expand Down Expand Up @@ -118,7 +118,7 @@ The API has a limited amount of requests you can make per time interval [Digikey
It is possible to retrieve the number of max requests and current requests by passing an optional api_limits kwarg to an API function:
```python
api_limit = {}
search_request = KeywordSearchRequest(keywords='CRCW080510K0FKEA', record_count=10)
search_request = KeywordSearchRequest(keywords='CRCW080510K0FKEA', limit=10)
result = digikey.keyword_search(body=search_request, api_limits=api_limit)
```

Expand All @@ -129,4 +129,29 @@ The dict will be filled with the information returned from the API:
'api_requests_remaining': 139
}
```
Sometimes the API does not return any rate limit data, the values will then be set to None.
Sometimes the API does not return any rate limit data, the values will then be set to `None`.


## Offsets
The api has a maximum of 50 parts in a request.
In order to gather more than the limit input into the `KeywordRequest` an offset must be input:
```python
# request items 51-100
search_request = KeywordRequest(keywords='RaspberryPi pico',limit = 50,offset=50)
```

This can also be used to gather all items to a request very simply:
```python
offset = 0
products = []

while(offset == 0 or (len(result.products) >= 49)):

search_request = KeywordRequest(keywords='RaspberryPi pico',limit = 50,offset=offset)
result = digikey.keyword_search(body=search_request)

for product in result.products:
products.append(product)

offset +=50
```
8 changes: 4 additions & 4 deletions digikey/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from digikey.v3.api import (keyword_search, product_details, digi_reel_pricing, suggested_parts,
manufacturer_product_details)
from digikey.v3.api import (status_salesorder_id, salesorder_history)
from digikey.v3.api import (batch_product_details)
from digikey.v4.api import (keyword_search, product_details, digi_reel_pricing, suggested_parts,
)
from digikey.v4.api import (status_salesorder_id, salesorder_history)
from digikey.v4.api import (batch_product_details)

name = 'digikey'
6 changes: 4 additions & 2 deletions digikey/oauth/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __init__(self,
version: int = 2,
sandbox: bool = False):

if version == 3:
if version == 3 or version == 4:
if sandbox:
self.auth_url = AUTH_URL_V3_SB
self.token_url = TOKEN_URL_V3_SB
Expand Down Expand Up @@ -252,7 +252,9 @@ def get_access_token(self) -> Oauth2Token:
('localhost', PORT),
lambda request, address, server: HTTPServerHandler(
request, address, server, self._id, self._secret))
httpd.socket = ssl.wrap_socket(httpd.socket, certfile=str(Path(filename)), server_side=True)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(str(Path(filename)))
httpd.socket=context.wrap_socket(httpd.socket,server_side=True,)
httpd.stop = 0

# This function will block until it receives a request
Expand Down
Empty file added digikey/v4/__init__.py
Empty file.
171 changes: 171 additions & 0 deletions digikey/v4/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import os
import logging
from distutils.util import strtobool
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Distutils is gone in python 3.12: https://docs.python.org/3/library/distutils.html, I'm not sure how this is working for you.

Copy link

@zmdev2082 zmdev2082 Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get this working with 3.12 I just used:

`
def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).

True values are 'y', 'yes', 't', 'true', 'on', and '1'; 
false values are 'n', 'no', 'f', 'false', 'off', and '0'.
Raises ValueError if 'val' is anything else.
"""
    val = val.lower()
    if val in ('y', 'yes', 't', 'true', 'on', '1'):
       return 1
    elif val in ('n', 'no', 'f', 'false', 'off', '0'):
        return 0
    else:
        raise ValueError(f"invalid truth value {val}")

`
The function is only called once in the library so there might be a more eloquent solution.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distutils.strtobool still exsits in Python 3.13.3. It's part of setuptools. hurricaneJoef#2 adds the missing dependency.

import digikey.oauth.oauth2
from digikey.exceptions import DigikeyError
from digikey.v4.productinformation import (KeywordRequest, KeywordResponse, ProductDetails, DigiReelPricing,
)
from digikey.v4.productinformation.rest import ApiException
from digikey.v4.ordersupport import (OrderStatusResponse, SalesOrderHistoryItem)
from digikey.v4.batchproductdetails import (BatchProductDetailsRequest, BatchProductDetailsResponse)

logger = logging.getLogger(__name__)


class DigikeyApiWrapper(object):
def __init__(self, wrapped_function, module):
self.sandbox = False

apinames = {
digikey.v4.productinformation: 'products',
digikey.v4.ordersupport: 'OrderDetails',
digikey.v4.batchproductdetails: 'BatchSearch'
}

apiclasses = {
digikey.v4.productinformation: digikey.v4.productinformation.ProductSearchApi,
digikey.v4.ordersupport: digikey.v4.ordersupport.OrderDetailsApi,
digikey.v4.batchproductdetails: digikey.v4.batchproductdetails.BatchSearchApi
}

apiname = apinames[module]
apiclass = apiclasses[module]

# Configure API key authorization: apiKeySecurity
configuration = module.Configuration()
configuration.api_key['X-DIGIKEY-Client-Id'] = os.getenv('DIGIKEY_CLIENT_ID')

# Return quietly if no clientid has been set to prevent errors when importing the module
if os.getenv('DIGIKEY_CLIENT_ID') is None or os.getenv('DIGIKEY_CLIENT_SECRET') is None:
raise DigikeyError('Please provide a valid DIGIKEY_CLIENT_ID and DIGIKEY_CLIENT_SECRET in your env setup')

# Use normal API by default, if DIGIKEY_CLIENT_SANDBOX is True use sandbox API
configuration.host = 'https://api.digikey.com/' + apiname + '/v4'
try:
if bool(strtobool(os.getenv('DIGIKEY_CLIENT_SANDBOX'))):
configuration.host = 'https://sandbox-api.digikey.com/' + apiname + '/v4'
self.sandbox = True
except (ValueError, AttributeError):
pass

# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
# configuration.api_key_prefix['X-DIGIKEY-Client-Id'] = 'Bearer'

# Configure OAuth2 access token for authorization: oauth2AccessCodeSecurity
self._digikeyApiToken = digikey.oauth.oauth2.TokenHandler(version=3, sandbox=self.sandbox).get_access_token()
configuration.access_token = self._digikeyApiToken.access_token

# create an instance of the API class
self._api_instance = apiclass(module.ApiClient(configuration))

# Populate reused ids
self.authorization = self._digikeyApiToken.get_authorization()
self.x_digikey_client_id = os.getenv('DIGIKEY_CLIENT_ID')

self.wrapped_function = wrapped_function

@staticmethod
def _remaining_requests(header, api_limits):
try:
rate_limit = header['X-RateLimit-Limit']
rate_limit_rem = header['X-RateLimit-Remaining']

if api_limits is not None and type(api_limits) == dict:
api_limits['api_requests_limit'] = int(rate_limit)
api_limits['api_requests_remaining'] = int(rate_limit_rem)

logger.debug('Requests remaining: [{}/{}]'.format(rate_limit_rem, rate_limit))
except (KeyError, ValueError) as e:
logger.debug(f'No api limits returned -> {e.__class__.__name__}: {e}')
if api_limits is not None and type(api_limits) == dict:
api_limits['api_requests_limit'] = None
api_limits['api_requests_remaining'] = None

@staticmethod
def _store_api_statuscode(statuscode, status):
if status is not None and type(status) == dict:
status['code'] = int(statuscode)

logger.debug('API returned code: {}'.format(statuscode))

def call_api_function(self, *args, **kwargs):
try:
# If optional api_limits, status mutable object is passed use it to store API limits and status code
api_limits = kwargs.pop('api_limits', None)
status = kwargs.pop('status', None)

func = getattr(self._api_instance, self.wrapped_function)
logger.debug(f'CALL wrapped -> {func.__qualname__}')
api_response = func(*args, self.x_digikey_client_id, authorization = self.authorization, **kwargs)
self._remaining_requests(api_response[2], api_limits)
self._store_api_statuscode(api_response[1], status)

return api_response[0]
except ApiException as e:
logger.error(f'Exception when calling {self.wrapped_function}: {e}')
self._store_api_statuscode(e.status, status)


def keyword_search(*args, **kwargs) -> KeywordResponse:
client = DigikeyApiWrapper('keyword_search_with_http_info', digikey.v4.productinformation)

if 'body' in kwargs and type(kwargs['body']) == KeywordRequest:
logger.info(f'Search for: {kwargs["body"].keywords}')
logger.debug('CALL -> keyword_search')
return client.call_api_function(*args, **kwargs)
else:
raise DigikeyError('Please provide a valid KeywordSearchRequest argument')


def product_details(*args, **kwargs) -> ProductDetails:
client = DigikeyApiWrapper('product_details_with_http_info', digikey.v4.productinformation)

if len(args):
logger.info(f'Get product details for: {args[0]}')
return client.call_api_function(*args, **kwargs)


def digi_reel_pricing(*args, **kwargs) -> DigiReelPricing:
client = DigikeyApiWrapper('digi_reel_pricing_with_http_info', digikey.v4.productinformation)

if len(args):
logger.info(f'Calculate the DigiReel pricing for {args[0]} with quantity {args[1]}')
return client.call_api_function(*args, **kwargs)


def suggested_parts(*args, **kwargs) -> ProductDetails:
client = DigikeyApiWrapper('suggested_parts_with_http_info', digikey.v4.productinformation)

if len(args):
logger.info(f'Retrieve detailed product information and two suggested products for: {args[0]}')
return client.call_api_function(*args, **kwargs)


def status_salesorder_id(*args, **kwargs) -> OrderStatusResponse:
client = DigikeyApiWrapper('order_status_with_http_info', digikey.v4.ordersupport)

if len(args):
logger.info(f'Get order details for: {args[0]}')
return client.call_api_function(*args, **kwargs)


def salesorder_history(*args, **kwargs) -> [SalesOrderHistoryItem]:
client = DigikeyApiWrapper('order_history_with_http_info', digikey.v4.ordersupport)

if 'start_date' in kwargs and type(kwargs['start_date']) == str \
and 'end_date' in kwargs and type(kwargs['end_date']) == str:
logger.info(f'Searching for orders in date range ' + kwargs['start_date'] + ' to ' + kwargs['end_date'])
return client.call_api_function(*args, **kwargs)
else:
raise DigikeyError('Please provide valid start_date and end_date strings')


def batch_product_details(*args, **kwargs) -> BatchProductDetailsResponse:
client = DigikeyApiWrapper('batch_product_details_with_http_info', digikey.v4.batchproductdetails)

if 'body' in kwargs and type(kwargs['body']) == BatchProductDetailsRequest:
logger.info(f'Batch product search: {kwargs["body"].products}')
logger.debug('CALL -> batch_product_details')
return client.call_api_function(*args, **kwargs)
else:
raise DigikeyError('Please provide a valid BatchProductDetailsRequest argument')
37 changes: 37 additions & 0 deletions digikey/v4/batchproductdetails/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# coding: utf-8

# flake8: noqa

"""
Batch Product Details Api

Retrieve list of product details from list of part numbers # noqa: E501

OpenAPI spec version: v3

Generated by: https://github.com/swagger-api/swagger-codegen.git
"""


from __future__ import absolute_import

# import apis into sdk package
from digikey.v3.batchproductdetails.api.batch_search_api import BatchSearchApi

# import ApiClient
from digikey.v3.batchproductdetails.api_client import ApiClient
from digikey.v3.batchproductdetails.configuration import Configuration
# import models into sdk package
from digikey.v3.batchproductdetails.models.api_error_response import ApiErrorResponse
from digikey.v3.batchproductdetails.models.api_validation_error import ApiValidationError
from digikey.v3.batchproductdetails.models.associated_product import AssociatedProduct
from digikey.v3.batchproductdetails.models.basic_product import BasicProduct
from digikey.v3.batchproductdetails.models.batch_product_details_request import BatchProductDetailsRequest
from digikey.v3.batchproductdetails.models.batch_product_details_response import BatchProductDetailsResponse
from digikey.v3.batchproductdetails.models.iso_search_locale import IsoSearchLocale
from digikey.v3.batchproductdetails.models.kit_part import KitPart
from digikey.v3.batchproductdetails.models.limited_taxonomy import LimitedTaxonomy
from digikey.v3.batchproductdetails.models.media_links import MediaLinks
from digikey.v3.batchproductdetails.models.pid_vid import PidVid
from digikey.v3.batchproductdetails.models.price_break import PriceBreak
from digikey.v3.batchproductdetails.models.product_details import ProductDetails
6 changes: 6 additions & 0 deletions digikey/v4/batchproductdetails/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import absolute_import

# flake8: noqa

# import apis into api package
from digikey.v3.batchproductdetails.api.batch_search_api import BatchSearchApi
Loading