sanmar_sdk.ftp ============== .. py:module:: sanmar_sdk.ftp .. autoapi-nested-parse:: SanMar's SFTP server, and readers for the data files on it. Every reader is a generator: rows are parsed one at a time as the file is read, so even SanMar's largest files never sit in memory. Readers take a path to a local file or an open text stream, so the same code parses a file streamed by :class:`SanMarFTP` or one downloaded some other way. Submodules ---------- .. toctree:: :maxdepth: 1 /autoapi/sanmar_sdk/ftp/catalog/index /autoapi/sanmar_sdk/ftp/client/index /autoapi/sanmar_sdk/ftp/inventory/index /autoapi/sanmar_sdk/ftp/legacy/index /autoapi/sanmar_sdk/ftp/orders/index /autoapi/sanmar_sdk/ftp/pricing/index /autoapi/sanmar_sdk/ftp/readers/index Classes ------- .. autoapisummary:: sanmar_sdk.ftp.CatalogProduct sanmar_sdk.ftp.ProductInformation sanmar_sdk.ftp.ProductRecord sanmar_sdk.ftp.SanMarFTP sanmar_sdk.ftp.WarehouseInventory sanmar_sdk.ftp.SaleItem sanmar_sdk.ftp.TextFileProduct sanmar_sdk.ftp.TextFileRecord sanmar_sdk.ftp.HoldingLine sanmar_sdk.ftp.OrderFiles sanmar_sdk.ftp.ShipmentStatus sanmar_sdk.ftp.CustomerPrice sanmar_sdk.ftp.PriceChange Functions --------- .. autoapisummary:: sanmar_sdk.ftp.read_catalog sanmar_sdk.ftp.read_extended_catalog sanmar_sdk.ftp.read_product_information sanmar_sdk.ftp.read_active_products sanmar_sdk.ftp.read_warehouse_inventory sanmar_sdk.ftp.read_catalog_txt sanmar_sdk.ftp.read_pdd sanmar_sdk.ftp.read_sale_items sanmar_sdk.ftp.read_holding sanmar_sdk.ftp.read_shipment_status sanmar_sdk.ftp.render_order_files sanmar_sdk.ftp.render_release_file sanmar_sdk.ftp.read_customer_prices sanmar_sdk.ftp.read_price_changes Package Contents ---------------- .. py:class:: CatalogProduct(/, **data: Any) Bases: :py:obj:`ProductRecord` One style, color and size from ``SanMar_SDL_N.csv`` or ``SanMar_EPDD.csv``. .. py:attribute:: quantity :type: int | None :value: None Stock across all warehouses, capped by SanMar. Only the EPDD file has it. .. py:attribute:: category :type: str | None :value: None The category. In SDL files this holds every category, separated by semicolons. .. py:attribute:: subcategory :type: str | None :value: None .. py:attribute:: suggested_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: msrp :type: sanmar_sdk.base.Price :value: None .. py:attribute:: companion_styles :type: str | None :value: None .. py:attribute:: front_model_image_url :type: str | None :value: None .. py:attribute:: back_model_image_url :type: str | None :value: None .. py:attribute:: front_flat_image_url :type: str | None :value: None .. py:attribute:: back_flat_image_url :type: str | None :value: None .. py:attribute:: product_measurements :type: str | None :value: None .. py:attribute:: pms_color :type: str | None :value: None The Pantone (PMS) color. SanMar says it does not stand in for the product color. .. py:attribute:: gtin :type: str | None :value: None .. py:attribute:: decoration_spec_sheet :type: str | None :value: None .. py:class:: ProductInformation(/, **data: Any) Bases: :py:obj:`ProductRecord` One style, color and size from a SanMarPI file. .. py:attribute:: category :type: str | None :value: None .. py:attribute:: keywords :type: sanmar_sdk.base.CommaSeparated :value: None .. py:attribute:: piece_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: sale_start_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: sale_end_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: price_code :type: str | None :value: None The suggested retail pricing code. A/P is 50%, B/Q 45%, C/R 40%, D/S 35%, E/T 30%. .. py:attribute:: front_flat :type: str | None :value: None .. py:attribute:: back_flat :type: str | None :value: None .. py:attribute:: front_model :type: str | None :value: None .. py:attribute:: back_model :type: str | None :value: None .. py:attribute:: side_model :type: str | None :value: None .. py:attribute:: three_q_model :type: str | None :value: None .. py:class:: ProductRecord(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` The columns every SanMar product file shares. .. py:attribute:: unique_key :type: str SanMar's identifier for this style, color and size, and the PromoStandards part id. .. py:attribute:: style :type: str :value: None .. py:attribute:: catalog_color :type: str :value: None The catalog (mainframe) color, which web services and orders expect. .. py:attribute:: color_name :type: str | None :value: None The full color name, for display only. .. py:attribute:: size :type: str .. py:attribute:: inventory_key :type: int .. py:attribute:: size_index :type: int .. py:attribute:: title :type: str | None :value: None .. py:attribute:: description :type: str | None :value: None .. py:attribute:: brand :type: str | None :value: None .. py:attribute:: status :type: str | None :value: None Coming Soon, New, Regular, or Discontinued. Coming Soon rows may be incomplete. .. py:attribute:: available_sizes :type: str | None :value: None .. py:attribute:: price_text :type: str | None :value: None .. py:attribute:: piece_weight :type: decimal.Decimal | None :value: None Approximate weight per piece in pounds. .. py:attribute:: piece_price :type: sanmar_sdk.base.Price :value: None The price per piece for five pieces or fewer of one style and color. .. py:attribute:: case_price :type: sanmar_sdk.base.Price :value: None The price per piece when buying by the case. .. py:attribute:: case_size :type: int | None :value: None .. py:attribute:: map_price :type: sanmar_sdk.base.Price :value: None The minimum advertised price. .. py:attribute:: brand_logo_image :type: str | None :value: None .. py:attribute:: thumbnail_image :type: str | None :value: None .. py:attribute:: color_swatch_image :type: str | None :value: None .. py:attribute:: product_image :type: str | None :value: None .. py:attribute:: spec_sheet :type: str | None :value: None .. py:attribute:: color_square_image :type: str | None :value: None .. py:attribute:: color_product_image :type: str | None :value: None .. py:attribute:: color_product_image_thumbnail :type: str | None :value: None .. py:function:: read_catalog(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[CatalogProduct] Stream ``SanMar_SDL_N.csv`` (or ``SanMar_SDL_DI.csv``), one product at a time. .. py:function:: read_extended_catalog(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[CatalogProduct] Stream ``SanMar_EPDD.csv``, one product at a time, with total stock. .. py:function:: read_product_information(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[ProductInformation] Stream a SanMarPI bulk, delta, brand or category file, one product at a time. .. py:class:: SanMarFTP(customer_number: int | str, password: str, *, host: str = HOST, port: int = PORT, host_key: str | None = None, known_hosts: pathlib.Path | None = None, timeout: float = 30.0) A connection to SanMar's SFTP server. Use it as a context manager, and read files inside the ``with`` block:: with SanMarFTP(123456, "ftp-password", host_key="ssh-rsa AAAA...") as ftp: with ftp.catalog() as products: for product in products: ... :param customer_number: The SanMar customer number, which is the SFTP username. :param password: The FTP password SanMar issued. SanMar.com logins do not work here. :param host: SanMar's SFTP server. :param port: SanMar's SFTP server. :param host_key: The server's public key, to trust it without a known_hosts entry. :param known_hosts: A known_hosts file to trust instead of the system's. :param timeout: Seconds to wait for the connection. .. py:attribute:: host :value: 'ftp.sanmar.com' .. py:attribute:: port :value: 2200 .. py:method:: __enter__() -> Self Connect. .. py:method:: __exit__(exc_type: type[BaseException] | None, exc: BaseException | None, traceback: types.TracebackType | None) -> None Disconnect. .. py:method:: connect() -> None Connect and log in, verifying the server's host key. .. py:method:: close() -> None Disconnect. Safe to call more than once. .. py:property:: sftp :type: paramiko.sftp_client.SFTPClient The underlying paramiko SFTP session, for anything this class does not cover. .. py:method:: list_folder(folder: str = '') -> list[str] List a folder's entries by name, sorted. Folder names match case-insensitively. .. py:method:: find(folder: str, pattern: str) -> list[str] List the files in a folder whose names match a shell-style pattern, ignoring case. Names come back sorted by name. SanMar dates the brand and category files it writes on request as ``MM-DD-YYYY``, which does not sort by date; :meth:`newest` does:: ftp.find("SanMarPDD/SanMarPI", "Brand_OGIO_*.csv") .. py:method:: newest(folder: str, pattern: str) -> str Find the most recently dated file in a folder whose name matches a pattern. Files are compared by the ``MM-DD-YYYY`` date SanMar puts in the names of the brand and category files it writes on request, then by name:: ftp.newest("SanMarPDD/SanMarPI", "Brand_OGIO_*.csv") :raises ~sanmar_sdk.exceptions.NotFoundError: If no file matches. .. py:method:: resolve(path: str) -> str Find a path on the server, matching each part of it case-insensitively. SanMar's guides spell the same file ``SanMar_SDL_N.csv`` and ``Sanmar_SDL_N.csv``. An exact match wins over a case-insensitive one. :raises ~sanmar_sdk.exceptions.NotFoundError: If some part of the path does not exist; the error lists what does. .. py:method:: open(path: str, *, prefetch: bool = True) -> collections.abc.Iterator[IO[bytes]] Open a remote file for reading, as bytes. ``prefetch`` requests the whole file ahead of reading, which is much faster for reading straight through; turn it off for random access. .. py:method:: open_text(path: str, *, member: str | None = None, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[TextIO] Open a remote text file, or a text file inside a remote zip archive. For a ``.zip`` path, ``member`` names the file inside it; it may be left out when the archive holds a single ``.csv`` or ``.txt`` file. .. py:method:: download(path: str, destination: pathlib.Path) -> pathlib.Path Copy a remote file to ``destination`` (a file, or a folder to put it in). .. py:method:: stream[R](path: str, reader: collections.abc.Callable[[TextIO], collections.abc.Iterator[R]], *, member: str | None = None, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[collections.abc.Iterator[R]] Stream a remote file through one of this package's readers. The rows are read as the file downloads, and only while the ``with`` block is open:: with ftp.stream("SanMarPDD/sanmar_saleItems.txt", read_sale_items) as items: for item in items: ... .. py:method:: catalog() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]] Stream ``SanMar_SDL_N.csv``, SanMar's main product file. .. py:method:: extended_catalog() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.CatalogProduct]] Stream ``SanMar_EPDD.csv``: the product file, with stock across all warehouses. .. py:method:: warehouse_inventory() -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.inventory.WarehouseInventory]] Stream ``sanmar_dip.txt``: stock by warehouse, with current prices, refreshed hourly. .. py:method:: product_information(file_name: str) -> contextlib.AbstractContextManager[collections.abc.Iterator[sanmar_sdk.ftp.catalog.ProductInformation]] Stream a file the product information web service wrote to ``SanMarPDD/SanMarPI``. .. py:method:: upload_orders(batch: str, orders: collections.abc.Sequence[sanmar_sdk.orders.PurchaseOrder]) -> sanmar_sdk.ftp.orders.OrderFiles Upload a batch of orders, without releasing them for processing. SanMar acknowledges the batch with a Holding file (see :meth:`holding`) and holds the orders until :meth:`release_orders` releases them, for up to two weeks. These are production orders; SanMar has no test environment for SFTP ordering. .. py:method:: release_orders(batch: str, po_numbers: collections.abc.Sequence[str], *, release_number: int = 1, delay: float = 5.0) -> str Release some or all of an uploaded batch's orders for processing. Each release of the same batch needs the next ``release_number``. SanMar asks for a pause of several seconds between uploading a batch and releasing it; ``delay`` waits that long first. Returns the release file's name. .. py:method:: holding(batch: str) -> collections.abc.Iterator[collections.abc.Iterator[sanmar_sdk.ftp.orders.HoldingLine]] Stream SanMar's acknowledgement of an uploaded batch, once it exists. SanMar writes it to ``Holding`` within about 15 minutes of the upload, and moves it to ``Done`` once the orders are processed. The file is the one whose name is the batch name followed by ``Holding``, so batch ``06-07-2022-1`` never reads ``06-07-2022-10Holding.txt``. :raises ~sanmar_sdk.exceptions.NotFoundError: If there is no Holding file for the batch yet. .. py:class:: WarehouseInventory(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` One style, color and size at one warehouse. .. py:attribute:: inventory_key :type: int .. py:attribute:: size_index :type: int .. py:attribute:: style :type: str :value: None .. py:attribute:: catalog_color :type: str The catalog (mainframe) color, which web services and orders expect. .. py:attribute:: size :type: str .. py:attribute:: warehouse :type: sanmar_sdk.common.WarehouseNumber :value: None .. py:attribute:: quantity :type: int Stock at this warehouse, capped by SanMar. .. py:attribute:: unique_key :type: str | None :value: None SanMar's identifier for this style, color and size, and the PromoStandards part id. .. py:attribute:: piece_weight :type: decimal.Decimal | None :value: None .. py:attribute:: piece_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_size :type: int | None :value: None .. py:attribute:: piece_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: sale_start_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: sale_end_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: discontinued_code :type: str | None :value: None Who discontinued the product, ``S`` for SanMar or ``M`` for the mill. .. py:property:: discontinued :type: bool Whether SanMar or the mill has discontinued the product. .. py:function:: read_active_products(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[WarehouseInventory] Stream ``sanmar_activeproductsexport.txt``, one row at a time. .. py:function:: read_warehouse_inventory(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[WarehouseInventory] Stream ``sanmar_dip.txt`` (or ``sanmar_closeouts_dip.txt``), one row at a time. .. py:class:: SaleItem(/, **data: Any) Bases: :py:obj:`TextFileRecord` One style, color and size on sale, from ``sanmar_saleItems.txt``. .. py:attribute:: piece_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: sale_start_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: sale_end_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:class:: TextFileProduct(/, **data: Any) Bases: :py:obj:`TextFileRecord` One style, color and size from ``sanmar_pdd.txt`` or ``Catalog.txt``. .. py:attribute:: piece_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_price :type: sanmar_sdk.base.Price :value: None .. py:class:: TextFileRecord(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` The columns SanMar's older product text files share. .. py:attribute:: inventory_key :type: int .. py:attribute:: size_index :type: int :value: None .. py:attribute:: style :type: str :value: None The style number, as printed in SanMar's catalog. .. py:attribute:: catalog_color :type: str The catalog (mainframe) color, which web services and orders expect. .. py:attribute:: size :type: str .. py:attribute:: brand :type: str | None :value: None .. py:attribute:: mill_style :type: str | None :value: None The manufacturer's own style number. .. py:attribute:: description :type: str | None :value: None .. py:attribute:: extended_description :type: str | None :value: None .. py:attribute:: case_quantity :type: int | None :value: None .. py:attribute:: weight :type: decimal.Decimal | None :value: None Approximate weight per piece in pounds. .. py:attribute:: size_type :type: str | None :value: None SanMar's internal size code. .. py:attribute:: gtin :type: str | None :value: None .. py:function:: read_catalog_txt(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[TextFileProduct] Stream ``Catalog.txt``, one product at a time. .. py:function:: read_pdd(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[TextFileProduct] Stream ``sanmar_pdd.txt``, one product at a time. .. py:function:: read_sale_items(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[SaleItem] Stream ``sanmar_saleItems.txt``, one sale item at a time. .. py:class:: HoldingLine(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` One line of SanMar's acknowledgement of an SFTP order. .. py:attribute:: po_number :type: str :value: None .. py:attribute:: style :type: str .. py:attribute:: catalog_color :type: str :value: None .. py:attribute:: size :type: str .. py:attribute:: quantity :type: int :value: None .. py:attribute:: warehouse :type: sanmar_sdk.common.WarehouseNumber :value: None The warehouse SanMar will ship this line from. .. py:attribute:: available :type: bool :value: None Whether the stock is there. If not, SanMar's customer service calls about it. .. py:class:: OrderFiles The two files that place a batch of orders: names and contents. .. py:attribute:: cust_info_name :type: str .. py:attribute:: cust_info :type: str .. py:attribute:: details_name :type: str .. py:attribute:: details :type: str .. py:class:: ShipmentStatus(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` One line of one box in SanMar's daily shipment status file. .. py:attribute:: po_number :type: str :value: None .. py:attribute:: sales_order_number :type: str :value: None .. py:attribute:: ship_date :type: sanmar_sdk.base.SanMarDate .. py:attribute:: style :type: str .. py:attribute:: catalog_color :type: str :value: None .. py:attribute:: size :type: str .. py:attribute:: quantity :type: int :value: None .. py:attribute:: ship_from :type: str | None :value: None .. py:attribute:: ship_to_name :type: str | None :value: None .. py:attribute:: attention :type: str | None :value: None .. py:attribute:: ship_to_address1 :type: str | None :value: None .. py:attribute:: ship_to_address2 :type: str | None :value: None .. py:attribute:: ship_to_city :type: str | None :value: None .. py:attribute:: ship_to_state :type: str | None :value: None .. py:attribute:: ship_to_zip :type: str | None :value: None .. py:attribute:: ship_to_country :type: str | None :value: None .. py:attribute:: sub_total :type: sanmar_sdk.base.Price :value: None .. py:attribute:: freight :type: sanmar_sdk.base.Price :value: None .. py:attribute:: handling_fee :type: sanmar_sdk.base.Price :value: None .. py:attribute:: invoice_total :type: sanmar_sdk.base.Price :value: None .. py:attribute:: tracking_number :type: str | None :value: None .. py:attribute:: total_cases :type: int | None :value: None .. py:attribute:: box_number :type: int | None :value: None .. py:attribute:: description :type: str | None :value: None .. py:attribute:: inventory_key :type: int | None :value: None .. py:attribute:: size_index :type: int | None :value: None .. py:attribute:: invoice_attention :type: str | None :value: None .. py:attribute:: license_plate :type: str | None :value: None The license plate number on the box's label, for the packing slip service. .. py:function:: read_holding(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[HoldingLine] Stream a Holding file, one order line at a time. .. py:function:: read_shipment_status(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[ShipmentStatus] Stream a daily shipment status file, one line of one box at a time. .. py:function:: render_order_files(batch: str, orders: collections.abc.Sequence[sanmar_sdk.orders.PurchaseOrder]) -> OrderFiles Build the ``CustInfo.txt`` and ``Details.txt`` files for a batch of orders. :param batch: The batch name, unique for every batch ever sent. SanMar suggests the date and a running number for the day, such as ``06-07-2022-1``. :param orders: The purchase orders in the batch. Items must be named by :class:`~sanmar_sdk.common.SkuKey`, and each order needs a ship-to email. .. py:function:: render_release_file(batch: str, po_numbers: collections.abc.Sequence[str], release_number: int = 1) -> tuple[str, str] Build the file that releases some or all of a batch's orders, as ``(name, contents)``. Each release of a batch gets the next ``release_number``, starting at 1. PO numbers are checked as a :class:`~sanmar_sdk.orders.PurchaseOrder` checks its own. .. py:class:: CustomerPrice(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` The account's price for one style, color and size. .. py:attribute:: unique_key :type: str .. py:attribute:: inventory_key :type: int .. py:attribute:: size_index :type: int .. py:attribute:: style :type: str :value: None .. py:attribute:: catalog_color :type: str The catalog (mainframe) color, which web services and orders expect. .. py:attribute:: size :type: str .. py:attribute:: my_price :type: sanmar_sdk.base.Price The account's price, which is the case or sale price where there is no special pricing. .. py:attribute:: piece_weight :type: decimal.Decimal | None :value: None .. py:attribute:: piece_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_size :type: int | None :value: None .. py:attribute:: piece_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: case_sale_price :type: sanmar_sdk.base.Price :value: None .. py:attribute:: sale_start_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: sale_end_date :type: sanmar_sdk.base.SanMarDate | None :value: None .. py:attribute:: discontinued_code :type: str | None :value: None Who discontinued the product, ``S`` for SanMar or ``M`` for the mill. .. py:class:: PriceChange(/, **data: Any) Bases: :py:obj:`sanmar_sdk.base.Record` A changed price from ``sanmar_dpc.csv``. .. py:attribute:: unique_key :type: str .. py:attribute:: my_price :type: sanmar_sdk.base.Price .. py:function:: read_customer_prices(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[CustomerPrice] Stream ``sanmar_dp.csv`` or ``sanmar_dpIncentive.csv``, one row at a time. .. py:function:: read_price_changes(source: sanmar_sdk.ftp.readers.Source, *, encoding: str = 'utf-8-sig') -> collections.abc.Iterator[PriceChange] Stream ``sanmar_dpc.csv``, one changed price at a time.