Source code for minim.api.discogs._api.inventory

from __future__ import annotations
import csv
from email.message import Message
import io
from pathlib import Path
from typing import TYPE_CHECKING

from ..._shared import TTLCache
from ._shared import DiscogsResourceAPI

if TYPE_CHECKING:
    from typing import Any, IO


[docs] class InventoryAPI(DiscogsResourceAPI): """ Inventory Export and Inventory Upload API endpoints for the Discogs API. .. important:: This class is managed by :class:`~minim.api.discogs.DiscogsAPIClient` and should not be instantiated directly. """ __slots__ = () def _prepare_inventory_csv( self, inventory_csv: bytes | str | Path, /, *, required_fields: set[str], optional_fields: set[str], update: bool = False, ) -> tuple[str, IO[bytes], str]: """ Validate and prepare the inventory comma-separated values (CSV) payload. Parameters ---------- inventory_csv : bytes, str, or pathlib.Path; positional-only Path to, name of, or a byte string of the contents of an inventory CSV file. required_fields : set[str]; keyword-only Required fields. optional_fields : set[str]; keyword-only Optional or other allowable fields. update : bool; keyword-only; default: :code:`False` Whether the CSV is for an inventory update and one of the optional fields is required. Returns ------- filename : str CSV filename or :code:`"inventory.csv"` if a byte string is provided instead. obj : io.BufferedReader or io.BytesIO Binary stream of the CSV file or the user-provided byte string. content_type : str Content type (:code:`"text/csv"`). """ self._validate_type("inventory_csv", inventory_csv, bytes | str | Path) if (is_str := isinstance(inventory_csv, str)) or isinstance( inventory_csv, bytes ): inventory_csv = self._prepare_string( "inventory_csv", inventory_csv ) if is_str: try: inventory_csv = ( Path(inventory_csv).expanduser().resolve(True) ) except (FileNotFoundError, OSError): pass if isinstance(inventory_csv, Path): csv_filename = inventory_csv.name csv_obj = inventory_csv.open("rb") else: csv_filename = "inventory.csv" csv_obj = io.BytesIO( inventory_csv.encode("utf-8") if isinstance(inventory_csv, str) else inventory_csv ) csv_stream = io.TextIOWrapper(csv_obj) try: csv_reader = csv.DictReader(csv_stream) csv_headers = set(csv_reader.fieldnames or []) if missing_fields := required_fields - csv_headers: raise ValueError( "`inventory_csv` is missing the following required " f"field(s): {self._join_values(missing_fields)}." ) additional_fields = csv_headers - required_fields if update and not additional_fields: raise ValueError( "`inventory_csv` must have at least one optional " "field for an inventory update." ) if extra_fields := additional_fields - optional_fields: raise ValueError( "`inventory_csv` has the following extra or unsupported " f"field(s): {self._join_values(extra_fields)}." ) all_conditions = ( self._CONDITIONS | self._ADDITIONAL_SLEEVE_CONDITIONS ) for row in csv_reader: for key, value in row.items(): if value is not None or key in required_fields: match key: case "release_id" | "weight" | "format_quantity": self._validate_numeric(key, value, int, 0) case "price": self._validate_numeric(key, value, float, 0) case "media_condition": self._validate_type(key, value, str) if value not in self._CONDITIONS: raise ValueError( f"Invalid media condition {value!r}. " f"Valid values: {self._join_values(self._CONDITIONS)}." ) case "sleeve_condition": self._validate_type(key, value, str) if value not in all_conditions: raise ValueError( f"Invalid media condition {value!r}. " f"Valid values: {self._join_values(all_conditions)}." ) case "comments" | "external_id" | "location": self._validate_type(key, value, str) case "accept_offer": self._validate_type(key, value, str) if value not in {"N", "Y"}: raise ValueError( "Invalid `accept_offer` value " f"{value!r}. Valid values: 'N', 'Y'." ) finally: csv_stream.detach() csv_obj.seek(0) return csv_filename, csv_obj, "text/csv"
[docs] def export_my_inventory(self) -> str: """ `Inventory Export > Export Your Inventory <https://www.discogs.com/developers/#page:inventory-export, header:inventory-export-export-your-inventory>`_: Export the current user's Discogs marketplace inventory as comma-separated values (CSV). .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Returns ------- resource_url : str Resource URL for the inventory export. .. seealso:: :meth:`get_inventory_export` – Get information for an inventory export. :meth:`download_inventory_export` – Download an inventory export CSV. """ self._client._require_authentication("inventory.export_my_inventory") return self._client._request("POST", "inventory/export").headers[ "location" ]
[docs] @TTLCache.cached_method(ttl="user") def get_my_inventory_exports( self, *, limit: int | None = None, page: int | None = None ) -> dict[str, Any]: """ `Inventory Export > Get Recent Exports <https://www.discogs.com /developers/#page:inventory-export, header:inventory-export-get-recent-exports>`_: Get Discogs resource information for the current user's recent marketplace inventory exports. .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- limit : int; keyword-only; optional Maximum number of exports to return. **Valid range**: :code:`1` to :code:`100`. **API default**: :code:`50`. page : int; keyword-only; optional Page number. Use with `limit` to get the next page of exports. **Minimum value**: :code:`1`. **API default**: :code:`1`. Returns ------- exports : dict[str, Any] Page of Discogs metadata for the current user's recent inventory exports. .. admonition:: Sample response :class: response dropdown .. code-block:: { "items": [ { "id": <int>, "created_ts": <str>, "download_url": <str>, "filename": <str>, "finished_ts": <str>, "status": <str>, "url": <str>, } ], "pagination": { "items": <int>, "page": <int>, "pages": <int>, "per_page": <int>, "urls": { "first": <str>, "last": <str>, "next": <str>, "prev": <str> } } } """ self._client._require_authentication( "inventory.get_my_inventory_exports" ) return self._get_paginated_resources( "inventory/export", limit=limit, page=page )
[docs] @TTLCache.cached_method(ttl="user") def get_inventory_export(self, export_id: int | str, /) -> dict[str, Any]: """ `Inventory > Get an Export <https://www.discogs.com/developers /#page:inventory-export,header:inventory-export-get-an-export>`_: Get Discogs resource information for a marketplace inventory export. .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- export_id : int or str; positional-only Discogs ID of the inventory export. **Examples**: :code:`599632`, :code:`"16105411"`. Returns ------- export : dict[str, Any] Discogs metadata for an inventory export. .. admonition:: Sample response :class: response dropdown .. code-block:: { "id": <int>, "created_ts": <str>, "download_url": <str>, "filename": <str>, "finished_ts": <str>, "status": <str>, "url": <str>, } """ self._client._require_authentication("inventory.get_inventory_export") self._validate_number("export_id", export_id, int, 1) return self._client._request( "GET", f"inventory/export/{export_id}" ).json()
[docs] @TTLCache.cached_method(ttl="user") def download_inventory_export( self, export_id: int | str, /, *, target: str | Path | None = None ) -> bytes | Path: """ `Inventory Export > Download an Export <https://www.discogs.com /developers/#page:inventory-export, header:inventory-export-download-an-export>`_: Download the comma-separated values (CSV) for a Discogs marketplace inventory export. .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- export_id : int or str; positional-only Discogs ID of the inventory export. **Examples**: :code:`599632`, :code:`"16105411"`. target : str or pathlib.Path; keyword-only; optional Target directory or file. If provided, a CSV file is written in the specified folder or with the specified filename. Otherwise, the raw CSV data is returned. Returns ------- export_csv : bytes or pathlib.Path Raw CSV data or absolute path to the saved CSV file for the inventory export. """ self._client._require_authentication( "inventory.download_inventory_export" ) self._validate_number("export_id", export_id, int, 1) resp = self._client._request( "GET", f"inventory/export/{export_id}/download" ) if target is None: return resp.content target = Path(target).expanduser().resolve() _msg = Message() _msg["content-disposition"] = resp.headers["content-disposition"] original_filename = Path( _msg.get_param("filename", header="content-disposition") ) if target.is_dir(): target.mkdir(parents=True, exist_ok=True) target /= original_filename else: target.parent.mkdir(parents=True, exist_ok=True) if target.suffix != original_filename.suffix: target = Path(f"{target}{original_filename.suffix}") with open(target, "w") as f: f.write(resp.content) return target
[docs] def upload_inventory_additions( self, inventory_csv: bytes | str | Path, / ) -> str: """ `Inventory Upload > Add Inventory <https://www.discogs.com /developers/#page:inventory-upload, header:inventory-upload-add-inventory>`_: Add Discogs marketplace listings by uploading comma-separated values (CSV). .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. .. note:: Listings are marked as "For Sale". Currency information is pulled from the current user's marketplace settings. Parameters ---------- inventory_csv : bytes, str, or pathlib.Path; positional-only Path to, name of, or contents of a CSV file containing the listings to add. **Required fields**: :code:`release_id`, :code:`price`, :code:`media_condition`. **Optional fields**: :code:`sleeve_condition`, :code:`comments`, :code:`accept_offer`, :code:`location`, :code:`external_id`, :code:`weight`, :code:`format_quantity`. Returns ------- resource_url : str Resource URL to get Discogs metadata for the inventory upload. .. seealso:: :meth:`get_inventory_upload` – Get information for an inventory upload. """ self._client._require_authentication( "inventory.upload_inventory_additions" ) return self._client._request( "POST", "inventory/upload/add", files={ "upload": self._prepare_inventory_csv( inventory_csv, required_fields={"release_id", "price", "media_condition"}, optional_fields={ "sleeve_condition", "comments", "accept_offer", "location", "external_id", "weight", "format_quantity", }, ) }, ).headers["Location"]
[docs] def upload_inventory_updates( self, inventory_csv: bytes | str | Path, / ) -> str: """ `Inventory Upload > Change Inventory <https://www.discogs.com /developers/#page:inventory-upload, header:inventory-upload-change-inventory>`_: Update Discogs marketplace listings by uploading comma-separated values (CSV). .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. .. note:: Currency information is pulled from the current user's marketplace settings. Parameters ---------- inventory_csv : str or pathlib.Path; positional-only Path to, name of, or contents of a CSV file containing the listings to update. **Required field**: :code:`release_id`. **Optional fields**: :code:`price`, :code:`media_condition`, :code:`sleeve_condition`, :code:`comments`, :code:`accept_offer`, :code:`location`, :code:`external_id`, :code:`weight`, :code:`format_quantity`. Returns ------- resource_url : str Resource URL to get Discogs metadata for the inventory upload. .. seealso:: :meth:`get_inventory_upload` – Get information for an inventory upload. """ self._client._require_authentication( "inventory.upload_inventory_updates" ) return self._client._request( "POST", "inventory/upload/change", files={ "upload": self._prepare_inventory_csv( inventory_csv, required_fields={"release_id"}, optional_fields={ "price", "media_condition", "sleeve_condition", "comments", "accept_offer", "location", "external_id", "weight", "format_quantity", }, update=True, ) }, ).headers["Location"]
[docs] def upload_inventory_deletions( self, inventory_csv: bytes | str | Path, / ) -> str: """ `Inventory Upload > Delete Inventory <https://www.discogs.com /developers/#page:inventory-upload, header:inventory-upload-delete-inventory>`_: Delete Discogs marketplace listings by uploading comma-separated values (CSV). .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- inventory_csv : str or pathlib.Path; positional-only Path to, name of, or contents of a CSV file containing the listings to delete. **Required field**: :code:`listing_id`. Returns ------- resource_url : str Resource URL to get Discogs metadata for the inventory upload. .. seealso:: :meth:`get_inventory_upload` – Get information for an inventory upload. """ self._client._require_authentication( "inventory.upload_inventory_deletions" ) return self._client._request( "POST", "inventory/upload/delete", files={ "upload": self._prepare_inventory_csv( inventory_csv, required_fields={"listing_id"}, optional_fields={}, ) }, ).headers["Location"]
[docs] @TTLCache.cached_method(ttl="user") def get_my_inventory_uploads( self, *, limit: int | None = None, page: int | None = None ) -> dict[str, Any]: """ `Inventory Upload > Get Recent Uploads <https://www.discogs.com /developers/#page:inventory-upload, header:inventory-upload-get-recent-uploads>`_: Get Discogs resource information for the current user's recent marketplace inventory uploads. .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- limit : int; keyword-only; optional Maximum number of uploads to return. **Valid range**: :code:`1` to :code:`100`. **API default**: :code:`50`. page : int; keyword-only; optional Page number. Use with `limit` to get the next page of uploads. **Minimum value**: :code:`1`. **API default**: :code:`1`. Returns ------- uploads : dict[str, Any] Page of Discogs metadata for the current user's recent inventory uploads. .. admonition:: Sample response :class: response dropdown .. code-block:: { "items": [ { "created_ts": <str>, "filename": <str>, "finished_ts": <str>, "id": <int>, "results": <str>, "status": <str>, "type": <str> } ], "pagination": { "items": <int>, "page": <int>, "pages": <int>, "per_page": <int>, "urls": { "first": <str>, "last": <str>, "next": <str>, "prev": <str> } } } """ self._client._require_authentication( "inventory.get_my_inventory_uploads" ) return self._get_paginated_resources( "inventory/upload", limit=limit, page=page )
[docs] @TTLCache.cached_method(ttl="user") def get_inventory_upload(self, upload_id: int | str, /) -> dict[str, Any]: """ `Inventory Upload > Get an Upload <https://www.discogs.com /developers/#page:inventory-upload, header:inventory-upload-get-an-upload>`_: Get Discogs catalog information for a marketplace inventory upload. .. admonition:: User authentication :class: entitlement .. tab-set:: .. tab-item:: Required User authentication Access protected endpoints. Parameters ---------- upload_id : int or str; positional-only Discogs ID of the inventory upload. **Examples**: :code:`119615`, :code:`"119615"`. Returns ------- upload : dict[str, Any] Discogs metadata for the inventory upload. .. admonition:: Sample response :class: response dropdown .. code-block:: { "created_ts": <str>, "filename": <str>, "finished_ts": <str>, "id": <int>, "results": <str>, "status": <str>, "type": <str> } """ self._client._require_authentication("inventory.get_inventory_upload") self._validate_number("upload_id", upload_id, int, 1) return self._client._request( "GET", f"inventory/upload/{upload_id}" ).json()